Agent 架构设计

一、起因:LLM 一次调用不够

我做了个工具—— text2everything.vip 自然语言描述流程,LLM 出 Mermaid 代码,右边实时渲染。

第一版是单次调用:用户输入 → LLM → 出图。跑了几个月发现两类问题:

  • 语法错了没救 —— LLM 生成的代码看着对,Mermaid 渲染直接 Parse error,用户看到红色堆栈就走了。
  • 语义偏了不自知 —— 用户说”画个登录流程”,LLM 惯性给你加个”发送验证码”,既不问,也不告诉你它加了。

这两个问题都不是”再调 prompt”能治的:第一次失败得有第二次机会;补常识没错,但必须留痕才能审计。

所以第二版做成一个 agent:能多轮对话,能自己纠错,能记住用户说过什么。这篇讲一下这个 agent 长什么样。

二、核心洞察:这不是一个循环,是两个

看似”多轮生成 UML”是一个循环,拆开看是两个,目标完全不同:

1
2
3
4
5
外层 · 问清楚要画啥
用户说话 → 提取关键信息 → 发现哪里没说清 → 追问 → 补齐

内层 · 画出来 + 修
结构化需求 → 生成代码 → 校验 → 有错就修 → 输出

外层管 “信息够不够” ,内层管 “代码对不对” 。混在一起做的话,你会发现”这个歧义要问用户吗”永远没答案 —— 因为外层的歧义(用户没说)和内层的歧义(生成错了)是两回事。

三、Agent 是个团队,分工要明确

我把 agent 想象成一家律师事务所处理案子。每个角色只做一件事:

角色 做什么 不做什么
记录员 把用户说过的事实记进档案 不猜、不推断
审查员 看档案哪一栏是空的 不决定问不问
决策官 决定问不问用户、问哪个 不自己去问
律师 拿档案起草文件(生成 Mermaid) 但可以补合理假设
校对员 检查文件语法 + 有没有夹带私货 有错就退回让律师改
前台 按顺序叫各角色出场,维护整个档案 不做任何判断

一开始我把这些揉在一个 prompt 里,发现 debug 极其痛苦 —— 因为说不清是”记录员搞错了”还是”律师瞎猜了” 。拆开之后每个角色都是纯函数,给一个输入永远给一个输出,单独测试。

调用关系:前台按顺序叫各角色出场

6 个角色之间不是各自平级各干各的,而是一条流水线,前台是唯一的调度者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
用户消息

前台 (append 到对话历史)

记录员 ──→ 从对话历史里提取事实,填进档案

审查员 ──→ 看档案哪些格子空着,输出缺失清单

决策官 ──→ 拿(缺失清单 + 预算 + 问过啥)决定下一步

├─ 决定"问一个" ──→ 措辞员 ──→ 用户
│ ↑ (下一轮回到最上面)

└─ 决定"信息够了" ──→ 律师 ──→ 生成 Mermaid 代码

校对员 · 语法检查

├─ FAIL ──→ 内层修复循环(退回律师)

└─ OK ──→ 校对员 · 语义检查

发现夹带私货?

├─ 是 → 图 + 追问一起呈现

└─ 否 → 直接呈现图

用户

几个值得注意的点:

  • 前台是唯一有状态的,别的角色都是”输入 → 输出”的纯函数,不记得上一次。多轮对话的记忆全靠前台维护档案。
  • 决策官是唯一分叉点,整个 agent “问 or 画”就它一个人决定。
  • 内层修复循环发生在律师和校对员之间,不惊动用户 —— Mermaid 语法错的自愈就靠这一段。
  • 语义检查发现问题不阻塞出图,而是跟着图一起呈现给用户(“图给你了,顺便问一下我加的这个要不要?”),这样用户看着图判断更容易。

关键规矩:记录员保守 vs 律师可以猜

这是整个架构的地基,展开讲一下。

用户说”画个登录流程,用户输密码,系统验证后返回结果”。记录员输出:

1
2
3
4
5
6
7
8
{
actors: ["用户", "系统"],
events: [
{ from: "用户", to: "系统", action: "输入密码" },
{ from: "系统", to: "用户", action: "返回结果" },
],
sync: "unknown" // ← 用户没说同步还是异步,绝对不猜
}

sync: "unknown" 这个字段就是追问的信号 —— 审查员看到 unknown,决策官就决定要不要问。

如果记录员帮用户补了(比如”登录一般同步,填 sync”),这个信号就没了 —— agent 分不清”用户真说过 sync”和”我瞎猜的 sync”,追问逻辑就崩了。

律师就没这个约束了。生成 Mermaid 时,律师可以补”验证码服务”这种合理假设,但必须写进档案的”假设”栏。后面校对员会拿档案 + 假设对账 —— 图里出现的每个元素都必须能追溯到”用户说的”或”律师的假设”,找不到就叫”夹带私货”,单独拉出来问用户”我加了这个,要保留吗?”

这一步一确定,后面很多问题自然就解了 —— 尤其是”用户说过’不要 X’,下一轮 LLM 又画上了 X”这种坑。因为夹带私货能被自动检测出来,agent 会主动把它捞出来问用户,而不是甩给用户自己发现。

四、”要不要追问用户”这件事,只让一个人管

这是我最喜欢的一条决策:所有”问不问用户”的判断,集中在一个纯函数里,叫 Planner(决策官)。

  • 审查员 / 校对员只识别问题 —— 缺什么、错什么。
  • 决策官拿三样东西做决定:候选问题池 + 剩余预算 + 之前问过哪些。
  • 输出三选一:问一个 / 信息够了开始画 / 给用户看摘要让他确认

一个 session 有个全局预算(比如 5 轮 agent 主动提问)。用户主动改需求 / 修图 / 要求重画都不算消耗预算 —— 只有 agent 主动打扰用户才算。这一条防止用户”越用越怕说话”。

这个设计的实际收益:想改追问节奏、调预算策略,只改这一个函数。别的组件不用动。

五、走一个例子 · 4 轮对话

看架构不够直观,走一个真实场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Turn 1  用户:"画个用户注册流程时序图"
记录员 → { actors: ["用户"], events: [], sync: unknown }
审查员 → 缺 actors / events / sync
决策官(预算 5)→ 问 actors
Agent: "除了用户,还有哪些角色?"
[预算: 5 → 4]

Turn 2-3 用户答:前端 / 后端 / 数据库,以及各步骤
决策官逐轮问,预算 4 → 3 → 2

Turn 4 用户:"先画看看" ← 跳过澄清,直接生成,不消耗预算
律师第一次生成 → 惯性加了个"发邮件通知"(没人提过邮件)
校对员语法检查 → FAIL(未声明的邮件系统)
触发内层修复循环 → 律师二次生成,去掉邮件 → 语法 OK
校对员语义检查 → 发现 3 个未声明的响应箭头 → 私货警告
输出:图 + 一个提示"我加了完整响应流程,想保留吗?"

Turn 5 用户:"响应不要,只画到存数据库就行"
Agent 把"响应流程"记入"负事实"(用户明说不要的)
重新生成 → 没响应箭头了
[预算不消耗,这属于用户看图后核对,不算 agent 追问]

5 轮预算,agent 实际主动追问 3 次,剩余 2 轮备用。 这就是全局预算的弹性。

六、加新图类型的成本

系统支持 Sequence · Flowchart · ERD · State · Class · Mindmap · Gantt · Arch 八种。

加一种新图 = 新写 4 个角色的实现:

  • 记录员 —— 怎么从自然语言提取事实
  • 审查员 —— 哪些字段是必填,哪些是可选
  • 校对员 —— 什么算夹带私货
  • 措辞员 —— 怎么问用户(时序图问”参与者”,类图问”实体”,措辞不一样)

决策官 / 前台 / 状态存储都不用动,因为它们不关心画的是哪种图。

七、3 条我觉得值得抄的经验

  1. 两层循环拆开做 —— “问清楚”和”画出来”是两件事,共享数据但目标不同。混在一起会陷入”这个歧义要问吗”的死循环。
  2. 记录员保守 + 律师可以猜,分工要严格 —— 记录员帮用户补常识就等于把 agent 的判断力交给了 LLM 的直觉。律师的每个假设必须留档,校对员才能审。
  3. 所有 policy 集中在一个决策官纯函数里 —— 想调追问节奏、改预算策略,只动一处代码。别的角色都是”识别问题”,只有它做”决定要不要问”。

八、后续

近期在做的:

  • 自动分类置信度校准(用户不说图类型时,LLM 猜错的概率)
  • 每种图的”夹带私货”规则集扩充
  • 一键分享:把 prompt + spec + 生成图打包成链接,收到 issue 时能一键重放整个对话

如果你也在做类似的 LLM 多轮 agent(不管是画图 / 写 SQL / 生成 API mock),这三条我觉得最能救命。Agent 不是”用 LLM 多调几次”这么简单,是要设计一个能自己纠错、能记住负面反馈、能审计每个决策的团队。

欢迎大家试用 https://text2everything.vip/ 给我提 bug。