位置:首页 > 进阶教程 > 多轮对话生成架构图与智能体设计实践指南

多轮对话生成架构图与智能体设计实践指南

时间:2026-07-25  |  作者:318050  |  阅读:0

引子

前段时间做了一个小工具,能把自然语言直接转成架构图——用户输入一句话描述流程,LLM生成Mermaid代码,右边实时预览。用了几个月,反复踩坑,最后发现失败案例可以归纳为两类。

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

第一类:LLM生成的Mermaid代码看起来没问题,但mermaid.js一渲染就报Parse error,用户看到红色堆栈就走了。这个问题不是调整prompt就能解决的,模型偶尔就是会写错,能做的只能是让它错完还能自己修复。

第二类更隐蔽。用户说“画个登录流程”,LLM顺手补一个“发送验证码”进去,但既没问用户也没留档。图出来之后用户想改,但找不到锚点——中间那份需求根本没被显性化记录下来。

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

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

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

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

外层 · 需求澄清 Loop用户自然语言 → 提取事实 → 发现缺失 → 追问 → 更新需求↓需求达到可绘制状态内层 · UML 生成修复 Loop结构化 spec → 生成 UML → 语法 / 语义校验 → 修复 → 输出

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

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

二、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是唯一调度者。

用户消息↓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“问还是画”就它一个人决定,想调追问节奏改这一个函数。
  • 内层修复循环发生在Generator和Validator之间,不惊动用户。Mermaid语法自愈就靠这一段。
  • 语义校验不阻塞出图。发现擅自添加时,图和追问一起呈现——用户看着图判断,比看着抽象需求容易得多。

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

这是整个架构的地基。

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

Extractor输出:

{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。

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

type SequenceSpec = {actors: Actor[]events: Event[]sync: "sync" | "async" | "unknown"// 负事实(用户明确说"没有"的东西)actors_negative: Actor[]events_negative: EventPattern[]}

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

三、Planner独占预算决策

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

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对比。

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

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

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

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

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_confrontationsGenerator 生成期兜底 assumption┘ ↓Renderer 出图时一起呈现↓ User 确认↓升级到 spec / 撤销 + regenerate

维度Pipeline APipeline B
触发时机生成前生成后(附着于图)
决策者PlannerOrchestrator(自动附加)
消耗预算10
阻塞流程否(可跳过)
Question kindClarificationConfrontation
用户答“是”效果填 spec升级 assumption 到 spec
用户答“否”效果记 ask_log(回避)撤销 + regenerate + 写入负事实

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

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

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 轮对话

Turn 1User: "画个用户注册流程时序图"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: 54]Turn 2-3User: 前端 / 后端 / 数据库,以及各步骤Planner 逐轮问 events / sync[budget: 432]Turn 4User: "先画看看"← SkipToGenerate,不消耗 budgetGenerator #1 → 惯性加"发邮件通知"(spec / assumptions 都没有)Validator 语法校验 → FAIL(未声明的邮件系统)内层修复循环触发 → Generator #2 → 去掉邮件 → 语法 OKValidator 语义校验 → 3 个未声明的响应箭头 → HIGH 擅自添加→ 挂 pending Confrontation输出:图 + Confrontation "我加了完整响应流程,想保留吗?"Turn 5User: "响应不要,只画到存数据库就行"← 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。

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

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

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

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

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

来源:整理自互联网
免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多