关注点分离(Separation of Concerns)示例(AI Agent 架构、流程编排、数据契约、提示词工程)
📅 2026/7/26 9:26:28
👁️ 阅读次数
📝 编程学习
Agent 功能的架构约定图放 services/agent/graphs/,必须定义显式 TypedDict 输入/输出 state schema;prompt 一律放 services/agent/prompts/,不许内联在图代码里。
为什么要这么设计?
文章目录
- 这套 Agent 架构约定的设计意图
- 1. Graph 放 `services/agent/graphs/` + 显式 TypedDict State Schema
- 为什么不直接用 `dict` 或 `TypedDict` 内联在函数签名里?
- 2. Prompt 放 `services/agent/prompts/`,禁止内联
- ① 角色分离:写 Prompt 的 ≠ 写 Graph 的
- ② 版本管理 & A/B 实验
- ③ 可观测性 & 成本追踪
- ④ 防止 Prompt 与逻辑耦合
- 3. 整体目录结构的意图
- 一句话总结
这套 Agent 架构约定的设计意图
你描述的这条规范本质上是在做关注点分离(Separation of Concerns),把 Agent 系统拆成了三个正交的维度:流程编排、数据契约、提示词工程。下面逐条拆解为什么要这么做。
1. Graph 放services/agent/graphs/+ 显式 TypedDict State Schema
为什么不直接用dict或TypedDict内联在函数签名里?
# ❌ 松散做法 — 上下游节点靠"默契"传字段defnode_a(state:dict)->dict:state["foo"]="bar"returnstate# ✅ 规范做法 — 显式契约classAgentState(TypedDict):messages:list[BaseMessage]plan:strtool_results:list[dict]is_complete:bool| 好处 | 说明 |
|---|---|
| 编译期可检查 | Mypy / Pyright 能在 CI 里直接报出字段拼写错误、类型不匹配,而不是等到运行时KeyError |
| 节点间契约清晰 | 每个 node 的input → output一目了然,新人不用顺着整个 graph 追数据流 |
| Graph 可视化/序列化 | LangGraph 等框架依赖显式 state 做 checkpoint、time-travel、状态恢复,隐式 dict 做不到 |
| 可测试性 | 单测一个节点时,TypedDict 就是 mock 数据的 schema,不用猜该传什么 |
2. Prompt 放services/agent/prompts/,禁止内联
这是整条规范里最实用的一条,原因至少有四层:
① 角色分离:写 Prompt 的 ≠ 写 Graph 的
Prompt 工程师 / 产品人员 → 改 prompts/xxx.txt 后端工程师 → 改 graphs/xxx.pyPrompt 调优是高频迭代,如果 prompt 写在 Python 代码里,每次改个措辞都要触碰业务代码、跑完整 CI、有合并冲突风险。
② 版本管理 & A/B 实验
prompts/ planner_v1.txt planner_v2.txt ← 灰度实验,直接切文件 tool_router.txt summarizer.txt放在独立文件里,可以:
- 用 Git 对 prompt 单独做 diff / blame / 回滚
- 按版本命名做 A/B test
- 未来迁移到 Prompt 管理平台(LangSmith、Helicone 等)零成本
③ 可观测性 & 成本追踪
集中管理后,很容易加一层统一的 loader,顺带做:
- Token 计数 / 成本预估
- Prompt 注入检测
- 变量注入审计(哪些
{variable}被填充了什么值)
如果 prompt 散落在各个.py文件里,这些横切逻辑就没地方挂。
④ 防止 Prompt 与逻辑耦合
# ❌ 内联 — prompt 和流程控制混在一起,改 prompt 可能误改逻辑defplanner_node(state):response=llm.invoke(f"""你是一个规划助手。 用户的请求是:{state['input']}如果涉及代码,请调用 code_tool... # ← 这是 prompt 还是业务规则? """)...# ✅ 分离 — 各管各的# prompts/planner.txt# graphs/planner.pydefplanner_node(state:PlannerInput)->PlannerOutput:prompt=load_prompt("planner",input=state["input"])response=llm.invoke(prompt)...3. 整体目录结构的意图
services/agent/ ├── graphs/ ← 流程编排("怎么走") │ ├── main_agent.py │ └── sub_graphs/ ├── prompts/ ← 提示词("怎么说") │ ├── planner.txt │ └── tool_router.txt ├── tools/ ← 工具实现("用什么做") └── schemas.py ← TypedDict 定义("数据长什么样")这四个目录对应了 Agent 系统的四个独立变化频率:
| 目录 | 变化频率 | 改动人 |
|---|---|---|
graphs/ | 低频(架构定下来很少改) | 后端 |
schemas.py | 中频(新增字段时改) | 后端 |
prompts/ | 高频(持续调优) | Prompt 工程师 / 产品 |
tools/ | 中频(新增能力时改) | 后端 |
变化频率不同的东西不应该放在同一个文件里——这是软件设计里最朴素也最重要的原则之一。
一句话总结
这套约定的核心目的是:让 prompt 调优、图编排、数据契约三件事可以独立演进、独立测试、独立 review,互不拖累。在 Agent 系统这种 prompt 改动频率远高于代码的系统里,这种分离不是洁癖,而是生存需要。
编程学习
技术分享
实战经验