多轮对话生成架构图 Agent 设计实践

引子

前段时间做了一个把自然语言转成架构图的小工具 ------ 一句话描述流程,LLM 生成 Mermaid 代码,右边实时预览。用了几个月,反复收敛到两类失败:

一类是 LLM 生成的 Mermaid 看起来对,mermaid.js 一渲染报 Parse error,用户看到红色堆栈就走了。这个不是调 prompt 能治的,模型偶尔就是会写错,能做的是让它错完还能自愈。

另一类更隐蔽。用户说"画个登录流程",LLM 顺手补一个"发送验证码"进去,不问不留档。图出来之后用户想改,但没有锚点 ------ 中间那份需求根本没显性化。

这两个问题指向同一件事:单次调用不够。得在"提取需求"、"生成代码"、"校验"之间加一层结构,任何一步崩了都能兜住。第二版就是把它改成一个 Agent ------ 多轮追问、语法自愈、能记住用户明说过"不要"的东西。这篇讲这套架构长什么样。

支持八种图:Sequence · Flowchart · ERD · State · Class · Mindmap · Gantt · Architecture。两种模式:auto (LLM 自己猜要画哪种)+ manual(用户明说)。

一、这不是一个 Loop,是两个

多轮生成 UML 看起来是一个 Loop,拆开是两个,共享同一份状态,但目标完全不同。

vbnet 复制代码
外层 · 需求澄清 Loop
用户自然语言 → 提取事实 → 发现缺失 → 追问 → 更新需求
                                    ↓
                              需求达到可绘制状态

内层 · UML 生成修复 Loop
结构化 spec → 生成 UML → 语法 / 语义校验 → 修复 → 输出
维度 外层澄清 内层生成修复
输入 自然语言 结构化 spec
输出 结构化 spec Mermaid 代码
成功条件 schema 关键字段填齐 Parser 通过 + 语义 Validator 通过
决策者 Planner(问不问) Validator(修不修)
失败兜底 预算耗尽 → 强行 proceed + 记录 assumption 修复 3 次上限 → 降级呈现失败图

混在一起做的话,你会遇到"这个歧义要不要问用户"这个死循环 ------ 因为外层歧义(用户没说)和内层歧义(LLM 生成偏了)看起来都是"缺信息",实际处理路径完全不同。前者走追问,消耗预算;后者走内部修复,不打扰用户。

核心洞察:两层 Loop 的边界不是"外层管对话、内层管代码",而是"外层管信息够不够、内层管代码对不对"。拆开后每层有明确的成功条件、决策者、失败兜底,后面所有决策才有落脚处。

二、Agent 内部六角色分工

内部再拆,把 Agent 想象成一个团队。这个比喻是我落地过程中试出来最好用的一版,后面很多设计围绕它展开。

角色 做什么 明确不做什么
Extractor 从对话历史提取事实、填 spec 不猜、不推断,留 unknown
Evaluator 遍历 spec,识别缺失与歧义 不判断问不问
Planner 拿(缺失清单 + 预算 + ask_log)决定下一步 不自己去问用户
Generator 生成 Mermaid,可以补合理 assumption 但每个 assumption 必须留档 render_context
Validator 语法校验 + 擅自检测(spec-assumption 对账) 有错就退回 Generator 重试
Orchestrator 按顺序调度上述五个,维护 AgentState 不做任何判断

一开始我把这些揉在一个巨型 prompt 里,让 LLM 一次调用同时做提取、判断、生成。跑了两周开始 debug 就崩溃了 ------ 一段错误的输出说不清是 Extractor 记错、Evaluator 漏、还是 Generator 瞎猜。角色拆开之后每一步都是纯函数,输入定了输出就定了,能单独 mock、单独测。

Functional Core / Imperative Shell 是这个分工的正式说法 ------ Orchestrator 是唯一 stateful 组件,其他五个都是纯函数。

2.1 调用关系:严格顺序的流水线

六个角色不是并联,是一条严格顺序的流水线,Orchestrator 是唯一调度者。

scss 复制代码
用户消息
    ↓
  Orchestrator     (append 到对话历史,维护 AgentState)
    ↓
  Extractor  ────→ 从对话历史提取事实,填 spec
    ↓
  Evaluator  ────→ 遍历 spec,输出 MissingSlot 清单
    ↓
  Planner    ────→ (清单 + budget + ask_log) → Decision
    │
    ├─ AskClarification ──→ QuestionStrategy ──→ User
    │                                             ↑ (下一轮从最上面开始)
    │
    └─ Proceed ──→ Generator ──→ 生成 Mermaid
                                    ↓
                              Validator · 语法校验
                                    │
                                    ├─ FAIL → 内层修复循环(退回 Generator,上限 3)
                                    │
                                    └─ OK → Validator · 语义校验(spec-assumption 对账)
                                              ↓
                                        擅自添加?
                                              │
                                        ├─ 是 → 图 + Confrontation 一起呈现
                                        │
                                        └─ 否 → 直接呈现
                                                    ↓
                                                   User
  • Orchestrator 是唯一 stateful 组件。别的角色都是"输入 → 输出"的纯函数,不记得上一次。多轮对话的所有记忆全在 Orchestrator 维护的 AgentState 里 ------ conversation_history、spec、render_context、ask_log、budget。
  • Planner 是唯一分叉点。整个 Agent"问 or 画"就它一个人决定,想调追问节奏改这一个函数。
  • 内层修复循环发生在 Generator 和 Validator 之间,不惊动用户。Mermaid 语法自愈就靠这一段。
  • 语义校验不阻塞出图。发现擅自添加时,图和追问一起呈现 ------ 用户看着图判断比看着抽象需求容易得多。

2.2 关键规矩:Extractor 保守,Generator 可以猜

这是整个架构的地基。

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

Extractor 输出:

css 复制代码
{
  actors: ["用户", "系统"],
  events: [
    { from: "用户", to: "系统", action: "输入密码" },
    { from: "系统", to: "用户", action: "返回结果" },
  ],
  sync: "unknown"    // ← 用户没说同步/异步,绝对不能猜
}

sync: "unknown" 是追问的信号 ------ Evaluator 看到 unknown 标"G2 歧义",Planner 拿到就决定要不要问。

如果 Extractor"贴心"补了 sync: "sync"(理由:登录一般是同步),这个信号就消失了。Evaluator 看到具体值不会标歧义,Planner 就不会问。用户从头到尾没被问过同步异步,但图画的是同步。这是把 Agent 的判断力偷偷让渡给了 LLM 的直觉,还没有留痕。

Generator 没有这个约束。它可以补一个"验证码服务"进 Mermaid,但每个 assumption 必须写进 render_context.assumptions。Validator 后面拿 spec + assumptions 对账 ------ 图里出现的每个元素必须能追溯到"用户明说"或"Generator 的显式假设"。追溯不到的,叫擅自添加(unauthorized addition),Validator 会拉出来做 Confrontation。

一个 case:用户后来说"响应不要,只画到存数据库就行"。这句话触发的东西比较多 ------ 撤销响应箭头、重新生成。但重新生成时有个坑,LLM 惯性又给你补上响应箭头。不能靠 prompt 治(prompt 治得住第一次治不住 N 次),得靠数据结构。所以 spec 除了正事实还有一栏叫负事实:

bash 复制代码
type SequenceSpec = {
  actors: Actor[]
  events: Event[]
  sync: "sync" | "async" | "unknown"

  // 负事实(用户明确说"没有"的东西)
  actors_negative: Actor[]
  events_negative: EventPattern[]
}

Generator 的 prompt 每轮包含负事实 hint("用户明确表示不要以下元素:响应流程"),生成时主动规避。就算 Generator 忘了,Validator 兜住 ------ 负事实里出现的元素被再次擅自添加,直接判 CRITICAL。

核心洞察 :unknown 是一等公民。它区分了"用户真说过 sync" vs "我瞎猜的 sync"。这一步妥协了,后面 ask_log、budget、Confrontation 全部瓦解 ------ 因为 Agent 分不清自己是在追问用户没答过的问题、还是在追问自己胡编的字段。

三、Planner 独占预算决策

第二条架构决策:所有"问不问用户"的判断集中在一个纯函数里,叫 Planner。

css 复制代码
type PlannerDecision =
  | { kind: "AskClarification"; slot: MissingSlot }
  | { kind: "Proceed" }
  | { kind: "RequestConfirm" }

function plan(
  candidatePool: MissingSlot[],
  budget: number,
  askLog: PastQuestion[],
): PlannerDecision

输入契约:

  • Evaluator 和 Validator 只识别问题,不判断"该问不该问"。
  • Planner 是 Agent 里唯一的追问决策入口。
  • Planner 无状态,ask_log 由 Orchestrator 传入 ------ 让它能区分"未问过的 unknown" vs "问过没答的 unknown",避免重复追问。

一个 session 有个全局预算 (实测取 5)。只有 Agent 主动打扰用户才消耗预算 ------ 用户主动改需求、修图、要求重画、对 Confrontation 的回答,全部不扣预算。

区分之前我用的是"每轮对话消耗一个预算"。跑了一段时间发现用户越用越不敢说话 ------ 每次开口都可能触发追问,慢慢用户学会直接说"先画看看"跳过所有澄清。这不是我想要的,澄清是价值最高的一步,用户躲开澄清 = Agent 白干。改成"只有主动追问扣预算"之后用户敢说话了。

工程收益三条:

  • 可回放:序列化 AgentState 就能复现任意一轮决策。收到 issue 时能完整重放,不用猜"那次 conversation 长啥样"。
  • 可测:每个纯函数拿假的输入就能测边界 ------ budget=1 / ask_log 有 2 次回避 / spec 全空。
  • 可 fork:同一个 state 用两个不同 Planner 策略跑,直接 A/B 对比。

核心洞察:policy 集中在一个纯函数里,想调追问节奏、加分段预算(前 2 轮严格 / 后 3 轮松)、加"剩余预算 ≤ 1 只问最重要的",全部只改这一个函数,别的组件不动。

四、Two-Pipeline:预算之内的追问 vs 预算之外的核对

早期版本我把所有追问和核对都塞进 Planner,发现两类东西性质根本不一样:

  • 画之前发现缺关键信息 ------ 得阻塞用户,先问再画,得花预算。
  • 画之后发现擅自添加 ------ 图已经出了,让用户看着图核对更直观,不该花预算。

硬塞进一个 pipeline 会出问题 ------ 预算被后一类蚕食,前一类就没机会问关键的东西了。所以架构裂成两条 pipeline:

sql 复制代码
Pipeline A · pre-render · budget-gated · Planner 决策
  Evaluator MissingSlot → Planner → AskClarification → User
                              (budget - 1)

Pipeline B · post-render · budget-free · Orchestrator 自动附加
  低置信 MetaExtractor 推断              ┐
  Validator HIGH/CRITICAL 擅自添加       ├→ pending_confrontations
  Generator 生成期兜底 assumption        ┘         ↓
                                          Renderer 出图时一起呈现
                                                ↓
                                             User 确认
                                                ↓
                              升级到 spec / 撤销 + regenerate
维度 Pipeline A Pipeline B
触发时机 生成前 生成后(附着于图)
决策者 Planner Orchestrator(自动附加)
消耗预算 1 0
阻塞流程 否(可跳过)
Question kind Clarification Confrontation
用户答"是"效果 填 spec 升级 assumption 到 spec
用户答"否"效果 记 ask_log(回避) 撤销 + regenerate + 写入负事实

核心洞察:Budget 语义精确化到"用户在看到图之前被 Agent 主动打扰的次数"。图出来之后的所有核对都不算,用户就不会有"越用越怕说话"的心理压力。

五、加一种新图类型的成本

新图 = 新增 4 个 sealed permit 分支:

go 复制代码
type UmlSpecification =
  | SequenceSpec
  | ClassSpec
  | FlowchartSpec
  | StateSpec
  | ERDSpec
  | MindmapSpec
  | GanttSpec
  | ArchSpec   // ← 新加一种,只加这一行 + 4 个 permit

每种图独立实现四个组件:

  • Extractor ------ 如何从自然语言提取事实
  • Evaluator ------ 哪些字段是 G1 阻塞 / G2 歧义 / G3 精细
  • Validator ------ 擅自检测规则 + Severity 判定
  • QuestionStrategy ------ 措辞(时序图问"参与者",类图问"实体")

不动的组件:Planner、Orchestrator、AgentState、共享 enum、MetaExtractor(auto/manual 分类)、Rescope 逻辑。图类型的差异被"上下夹"的两层吸收掉了 ------ 输入侧由 Extractor / Evaluator / Validator 各自识别、转换为共享 enum;输出侧由 QuestionStrategy 把 payload 渲染成贴合图类型的自然语言。

六、一个完整 case · 5 轮对话

css 复制代码
Turn 1  User: "画个用户注册流程时序图"
        MetaExtractor → { type: Sequence, confidence: HIGH, source: USER_EXPLICIT }
        Extractor     → { actors: ["用户"], events: [], sync: unknown }
        Evaluator     → [actors_count G1, events G1, sync G2]
        Planner(budget=5) → AskClarification(actors_count)
        Agent: "除了用户,还有哪些角色参与?"
        [budget: 5 → 4]

Turn 2-3  User: 前端 / 后端 / 数据库,以及各步骤
          Planner 逐轮问 events / sync
          [budget: 4 → 3 → 2]

Turn 4  User: "先画看看"  ← SkipToGenerate,不消耗 budget
        Generator #1 → 惯性加"发邮件通知"(spec / assumptions 都没有)
        Validator 语法校验 → FAIL(未声明的邮件系统)
        内层修复循环触发 → Generator #2 → 去掉邮件 → 语法 OK
        Validator 语义校验 → 3 个未声明的响应箭头 → HIGH 擅自添加
                          → 挂 pending Confrontation
        输出:图 + Confrontation "我加了完整响应流程,想保留吗?"

Turn 5  User: "响应不要,只画到存数据库就行"  ← ConfrontationAnswer NO
        写入负事实:events_negative.append({action: "响应流程"})
        触发 regenerate → 无响应箭头 → 呈现新图
        [budget 不变,Confrontation = 0]

5 轮预算,Agent 实际主动追问 3 次,剩余 2 轮备用。这就是全局预算的弹性 ------ Planner 知道什么时候要收敛,不会把用户问烦。

七、几个取舍

LLM 场景下"纯函数"是近似真,不是数学真。Extractor 描述我一开始写的是"精确幂等",跑了几次发现 LLM 每次输出都有小方差,重跑结果不完全一致 ------ 用户会觉得"Agent 忘了我上一轮说过的话"。工程上用低 temperature + 严格 schema + 每轮 diff 提示("根据你最新的说法,我重新理解了整段对话")能缓解,但架构层的诚实做法是承认这一点。描述改成"结构近似幂等,语义相似性尽力而为"。

Confrontation "否" 的默认值取 LLM top-1,不引入业务权重。MetaExtractor 会给多个候选(比如"这可能是时序图 / 状态图 / 流程图"),置信度最高的作为默认。不引入业务规则加权(比如"程序员多写时序图,给 sequence 加权"),保持 debug 简单 ------ 出错时你知道模型给的原始分布是啥,不用查规则表。

类型中途切换用 Rescope,不删对话历史,只重跑 Extractor。用户 Turn 3 说"换成状态图吧",累积的 SequenceSpec 怎么办?三个候选:Discard(丢弃)简单但信息白费用户会怒;Migrate(映射)N² 映射规则,错映射比丢弃还糟;Rescope 保留 conversation_history + ask_log + budget,清空 spec + render_context + pending_confrontations,重跑 StateExtractor 派生新 spec。

核心洞察:conversation_history 是唯一权威,spec 是从历史派生出来的视图。换图类型 = 换派生视图,历史不动。

八、如果你也在做多轮 LLM Agent

不管你做的是画图、写 SQL、生成 API mock,这三条我觉得最能省事:

两层嵌套 Loop 拆开做。"问清楚"和"画出来"是两件事,共享数据但目标不同。混在一起会陷入"这个歧义要问吗"的死循环。

Extractor 保守 + Generator 可以猜,分工严格。这是"擅自检测能工作"的地基。Extractor 帮用户补常识就等于把 Agent 的判断力交给了 LLM 的直觉;Generator 的每个 assumption 必须留档 render_context.assumptions,Validator 才能对账。

所有 policy 集中在一个 Planner 纯函数里。想调追问节奏、改预算策略、加分段预算 ------ 只动一处代码。别的角色都是"识别问题",只有 Planner 做"决定要不要问"。工程收益(可回放 / 可测 / 可 fork)三条全靠这个分工吃到。

工具体验地址text2everything.vip

相关推荐
吴佳浩2 小时前
今天我们讲讲大模型的“核心”技术:蒸馏(Model Distillation)
人工智能·llm·agent
阿里云大数据AI技术2 小时前
阿里云 ES AI 引擎版:面向 Agent 场景,为亿级租户、千亿规模向量设计的搜索引擎
人工智能·elasticsearch·agent
星栈2 小时前
翻完 Pi 源码:它和 Codex、Claude Code 有何不同
人工智能·agent
码哥字节3 小时前
Google 上周推了个 agents-cli,我装完发现 Claude Code 多了 7 个超能力
google·agent·claude
奋飛5 小时前
AI应用工程:Agent 的能力是如何扩展的?——Tool、Skill、Workflow 与 MCP 的职责边界
agent·workflow·mcp·skills·ai应用工程
小阿鑫5 小时前
一个 AI 应用开发程序员的一天,都在屏幕前忙些什么?
ai·程序员·agent·rd270q·明基rd270q
武子康6 小时前
Token 单价更低,Agent 任务为什么反而更贵:4 层成本口径 + 最小事件账本 + 3 个决策问题
人工智能·agent·ai编程
武子康6 小时前
Shippy 全拆:3 个集合收缩(动作 / 状态 / 后果)+ 4 类边界(版本化行为 / Typed API / CLI / Sandbox)
人工智能·agent·aiops
辞忧九千七7 小时前
MCP 协议完全指南:从原理到 LangGraph 集成,打造即插即用的 AI Agent 工具生态
agent·langgraph·mcp