<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>LangGraph on 姚玉亮的网络日志</title><link>https://yusanwen-code.github.io/tags/langgraph/</link><description>Recent content in LangGraph on 姚玉亮的网络日志</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><lastBuildDate>Wed, 02 Sep 2026 20:00:00 +0800</lastBuildDate><atom:link href="https://yusanwen-code.github.io/tags/langgraph/index.xml" rel="self" type="application/rss+xml"/><item><title>对话编排层重构：从 Go 业务编排迁移到 Python LangGraph</title><link>https://yusanwen-code.github.io/posts/post-76/</link><pubDate>Wed, 02 Sep 2026 20:00:00 +0800</pubDate><guid>https://yusanwen-code.github.io/posts/post-76/</guid><description>&lt;img src="https://yusanwen-code.github.io/images/post-76-cover.jpg" alt="Featured image of post 对话编排层重构：从 Go 业务编排迁移到 Python LangGraph" /&gt;&lt;h2 id="一条报数指令暴露的问题"&gt;一条报数指令暴露的问题&#10;&lt;/h2&gt;&lt;p&gt;炼丹炉（&lt;a class="link" href="https://github.com/yusanwen-code/alchemy-furnace" target="_blank" rel="noopener"&#10; &gt;Alchemy Furnace&lt;/a&gt;，我的开源项目）的群聊里，我发过这样一条指令：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&#10;&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;1&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@全体成员 全体都有！报数！&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;预期很明确：A、B、C、D 四个道人按顺序报 1、2、3、4。实际跑起来，它被当成了闲聊：有时只让一个人接话，有时人设和记忆指令还能把硬指令覆盖掉，报数报成了自由发挥。&lt;/p&gt;&#10;&lt;p&gt;这不是一个偶发 bug，是一类问题的缩影：当编排逻辑长在业务代码里，&amp;ldquo;确定性指令&amp;quot;和&amp;quot;开放对话&amp;quot;就没有边界。群聊调度全部写在 Go 网关里，意图分类、发言人选择、轮次策略和业务逻辑缠在一起，每加一个编排能力都要改 Go、编译、发布，而编排恰恰是整个产品里变化最快的部分。&lt;/p&gt;&#10;&lt;p&gt;于是我做了这次架构迁移：把提示词构建、群聊调度和模型调用编排，整体从 Go 迁到 Python LangGraph，形成「Go 负责数据与 CRUD，Python 负责智能编排」的分层。&lt;/p&gt;&#10;&lt;h2 id="go-在变成-agent-框架"&gt;Go 在变成 Agent 框架&#10;&lt;/h2&gt;&lt;p&gt;迁移前炼丹炉是双后端：Go 网关（Gin）+ Python 引擎（FastAPI）。这个结构本身没问题，问题在于群聊上线后，Go 里长出了第三种性质的职责。&lt;/p&gt;&#10;&lt;p&gt;一轮群聊对话，Go 实际在做三件事：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;数据：会话、消息、道人、金丹、记忆、模型的存储与 CRUD，凭据加密，这是 Go 的本行&lt;/li&gt;&#10;&lt;li&gt;协议：对前端的 SSE 流、内部服务调用，是稳定的契约面&lt;/li&gt;&#10;&lt;li&gt;智能编排：意图分类、发言人选择、提示词拼装、模型调用、轮次与收敛策略，这部分高频变化、需要快速实验&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;第三类和前两类完全不是一种东西。编排层需要的是图结构、状态机、检查点、可恢复执行、结构化输出校验，这些 LangGraph 生态里全是现成的，Go 里全要手写。让一个后端语言慢慢长成一个 Agent 框架，才是最贵的路线。&lt;/p&gt;&#10;&lt;h2 id="分层go-管什么python-管什么"&gt;分层：Go 管什么，Python 管什么&#10;&lt;/h2&gt;&lt;p&gt;迁移后的职责边界：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;职责&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;归属&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;认证、请求校验、凭据加密存储&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Go&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;会话/消息/道人/金丹/记忆/模型的存储与 CRUD&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Go&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;正式消息落库（只在 &lt;code&gt;assistant_final&lt;/code&gt; 之后，按 run_id 幂等）&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Go&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;内部事件 → 前端 SSE 的翻译&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Go&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;单聊/群聊路由&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Python LangGraph&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;指令分类、确定性路由&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Python&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Supervisor 计划、发言人调度、收敛&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Python&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;提示词构建、模型调用、重试&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Python&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;记忆检索策略与记忆提案&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Python 提案 → Go 校验落库&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;说白了：Go 不再选择发言人、不再分类对话意图、不再拼 prompt，也不再管轮次策略。前端仍然只调 Go、只消费 Go 的 SSE。对前端来说契约没变，变的是 SSE 背后的调度从 Go 挪进了图里。&lt;/p&gt;&#10;&lt;p&gt;边界里我最在意的一条：凭据不进图状态。API key、解密后的凭据走 LangGraph 的 &lt;code&gt;context_schema&lt;/code&gt;（RuntimeContext：模型网关、凭据、调试开关、取消旗标），与图状态彻底分离：它们绝不进入图状态、SQLite checkpoint、对外事件和日志。图状态里只有快照：历史、道人、金丹、记忆、模型引用，都是 Go 在发起一轮 run 时传进来的不可变快照。&lt;/p&gt;&#10;&lt;h2 id="图架构一张会话图一张可复用的道人图"&gt;图架构：一张会话图，一张可复用的道人图&#10;&lt;/h2&gt;&lt;p&gt;整个编排是四层图，顶层一张会话图：&lt;/p&gt;&#10;&#10;&lt;pre class="mermaid"&gt;&#10;flowchart TD&#10; A[&amp;#34;ConversationGraph（一次用户轮入口）&amp;#34;] --&amp;gt; B[&amp;#34;hydrate_context&amp;#34;] --&amp;gt; C[&amp;#34;route_session&amp;#34;]&#10; C --&amp;gt; D[&amp;#34;SingleChatGraph&amp;#34;] --&amp;gt; D1[&amp;#34;DaoistGraph（复用子图）&amp;#34;]&#10; C --&amp;gt; E[&amp;#34;GroupChatGraph&amp;#34;] --&amp;gt; F[&amp;#34;classify_directive&amp;#34;]&#10; F --&amp;gt; G[&amp;#34;deterministic_router（确定性指令）&amp;#34;]&#10; F --&amp;gt; H[&amp;#34;supervisor（开放讨论出计划）&amp;#34;]&#10; G --&amp;gt; I[&amp;#34;dispatch_daoists&amp;#34;]&#10; H --&amp;gt; I&#10; I --&amp;gt; J[&amp;#34;DaoistGraph × N&amp;#34;]&#10; I --&amp;gt; K[&amp;#34;convergence 收敛&amp;#34;]&#10; K --&amp;gt; L[&amp;#34;propose_memory 记忆提案 → Go 校验落库&amp;#34;]&#10;&lt;/pre&gt;&#10;&#10;&lt;p&gt;关键设计是 DaoistGraph 作为可复用子图：单聊直接调它，群聊每个发言人各自子运行一张。单聊和每个群成员走的是同一条 prompt 构建 + 模型调用管线：选定记忆、编译人设与金丹行为、调模型、校验响应、流出事件。这一条保证了&amp;quot;行为可解释&amp;rdquo;：单聊什么效果，群聊里同一个道人就是什么效果，不存在两套 prompt 逻辑。&lt;/p&gt;&#10;&lt;p&gt;群聊图里的路由是有讲究的：&lt;code&gt;classify_directive&lt;/code&gt; 先分类这一轮用户输入，确定性指令走 &lt;code&gt;deterministic_router&lt;/code&gt; 直接成计划，开放讨论才走 &lt;code&gt;supervisor&lt;/code&gt; 让模型出计划，这就是下一节的内容。&lt;/p&gt;&#10;&lt;h2 id="混合导演该调模型的调模型不该调的绝不调"&gt;混合导演：该调模型的调模型，不该调的绝不调&#10;&lt;/h2&gt;&lt;p&gt;群聊调度我用的是混合导演策略，核心就一句：确定性的事情用代码，创造性的事情用模型。&lt;/p&gt;&#10;&lt;p&gt;确定性指令永不委托模型。直接 @某个成员、@全体成员、报数、停止/继续，这些必须在代码里走死路径。报数的实现是：路由器为每个成员构造一个不可变任务，里面写死序号；人设、记忆、闲聊风格规则只能影响语气，不能改数字、不能漏人、不能加戏、不能改顺序。报数这种事交给模型去&amp;quot;理解&amp;quot;，就是开头那个 bug 的根源。&lt;/p&gt;&#10;&lt;p&gt;开放讨论交给 Supervisor。这一轮该谁说话、按什么顺序、每人拿到什么任务、发言预算多少、要不要收束，由会话默认模型（非人设，不扮演任何道人）产出一份结构化计划。Supervisor 不可见、不写用户可见的文本，只出计划。它失败或产出无效计划时，确定性回退到被明确 @ 的成员或主成员，导演的错误不传染整轮。&lt;/p&gt;&#10;&lt;p&gt;这样分工还有个实际的好处：明确的群指令不花 Supervisor 的模型调用，单聊用被选中道人自己配置的模型。钱花在真正需要智能的地方。&lt;/p&gt;&#10;&lt;h2 id="可恢复执行中断在发言人边界续跑按差集"&gt;可恢复执行：中断在发言人边界，续跑按差集&#10;&lt;/h2&gt;&lt;p&gt;这是整个迁移里技术含量最高的部分，也是最值得抄的部分。&lt;/p&gt;&#10;&lt;p&gt;每个用户轮有全局唯一的 &lt;code&gt;run_id&lt;/code&gt;，每条终稿回复有唯一 &lt;code&gt;reply_id&lt;/code&gt;，Go 按这两个 ID 做落库幂等。LangGraph 侧用本地异步 SQLite checkpointer 做检查点。用户点停止、前端断连、进程重启，都要能恢复，而且恢复时不能重复已完成发言人的发言。&lt;/p&gt;&#10;&lt;p&gt;做法是把恢复粒度设计在发言人边界：dispatch 节点按计划逐个发言人在子图里跑，每个发言人之前先 &lt;code&gt;await asyncio.sleep(0)&lt;/code&gt; 让出事件循环再查取消旗标：取消请求只能落在 await 点，这样中断恰好落在发言人之间的缝隙里，不会截断一条发到一半的回复。&lt;/p&gt;&#10;&lt;p&gt;续跑靠差集裁决。顶层检查点只携带「计划 + 已完成回复」，恢复重放时：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;计划已存在 → 不重新规划、不重发 &lt;code&gt;plan_created&lt;/code&gt; 事件&lt;/li&gt;&#10;&lt;li&gt;已完成的发言人 → 跳过，只在剩余差集上继续&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;其中藏着一个容易踩的坑，代码里的路由顺序：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&#10;&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;1&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;2&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;3&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;4&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;5&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;6&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;7&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff7b72"&gt;def&lt;/span&gt; &lt;span style="color:#d2a8ff;font-weight:bold"&gt;_route_after_router&lt;/span&gt;(state) &lt;span style="color:#ff7b72;font-weight:bold"&gt;-&amp;gt;&lt;/span&gt; Literal[&lt;span style="color:#a5d6ff"&gt;&amp;#34;dispatch&amp;#34;&lt;/span&gt;, &lt;span style="color:#a5d6ff"&gt;&amp;#34;supervisor&amp;#34;&lt;/span&gt;, &lt;span style="color:#a5d6ff"&gt;&amp;#34;end&amp;#34;&lt;/span&gt;]:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;if&lt;/span&gt; state&lt;span style="color:#ff7b72;font-weight:bold"&gt;.&lt;/span&gt;get(&lt;span style="color:#a5d6ff"&gt;&amp;#34;speaking_plan&amp;#34;&lt;/span&gt;):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;&amp;#34;dispatch&amp;#34;&lt;/span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;# 续跑重放：计划已在，按差集续，不重规划&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; kind &lt;span style="color:#ff7b72;font-weight:bold"&gt;=&lt;/span&gt; state[&lt;span style="color:#a5d6ff"&gt;&amp;#34;directive&amp;#34;&lt;/span&gt;][&lt;span style="color:#a5d6ff"&gt;&amp;#34;kind&amp;#34;&lt;/span&gt;]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;if&lt;/span&gt; kind &lt;span style="color:#ff7b72;font-weight:bold"&gt;==&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;&amp;#34;open&amp;#34;&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;&amp;#34;supervisor&amp;#34;&lt;/span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;# 首轮开放讨论：默认模型出计划&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff7b72"&gt;return&lt;/span&gt; &lt;span style="color:#a5d6ff"&gt;&amp;#34;end&amp;#34;&lt;/span&gt; &lt;span style="color:#8b949e;font-style:italic"&gt;# 停止/继续：控制指令，不产计划&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;这个判断顺序不能反。如果先看 &lt;code&gt;kind&lt;/code&gt;，开放讨论在续跑重放时会误入 supervisor 再调一次模型；先看计划有无，重放才直接走 dispatch 按差集续跑。可恢复执行的全部难度就在这种毫厘之间：状态里每多带一个通道、路由里每换一个判断顺序，恢复语义就完全不同。&lt;/p&gt;&#10;&lt;p&gt;失败语义同样是发言人粒度：单个道人失败只跳过该发言人，不阻塞剩余计划成员；道人图内部对模型错误做最多 2 次机械重试；Supervisor 失败走确定性回退。三层兜底，任何一层的失败都不会把整轮炸掉。&lt;/p&gt;&#10;&lt;h2 id="迁移不是重写feature-flag--一条真实的提交序列"&gt;迁移不是重写：feature flag + 一条真实的提交序列&#10;&lt;/h2&gt;&lt;p&gt;迁移策略是增量推进，但结局只有一种实现：feature flag &lt;code&gt;orchestration_engine=legacy|langgraph&lt;/code&gt; 是临时迁移控制，不是长期双引擎产品设置。验证通过后默认 LangGraph，短回滚窗口过后删掉 legacy。&lt;/p&gt;&#10;&lt;p&gt;真实提交序列（节选自仓库主干）：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;div style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&#10;&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 1&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 2&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 3&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 4&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 5&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 6&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 7&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 8&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt; 9&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;10&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;11&#10;&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679"&gt;12&#10;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#10;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;&#10;&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docs: design LangGraph conversation orchestration ← 321 行设计文档&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docs: plan LangGraph orchestration migration ← 873 行实施计划&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;build(python): add LangGraph runtime dependencies&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;feat(orchestration): define state and event contracts ← 状态/事件契约先行&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;feat(orchestration): add provider-aware model gateway&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;feat(orchestration): evolve speaking plan to ordered task items&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;feat(orchestration): route explicit group directives&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;feat(orchestration): add reusable Daoist graph&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;feat(orchestration): add hybrid group director&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;feat(orchestration): add resumable conversation graph&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;feat(orchestration): expose internal streaming API&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;feat(chat): persist orchestration runs idempotently&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;/div&gt;&lt;p&gt;节奏是契约先行：状态、事件、模型网关先于任何图节点落地；图是一层一层长出来的（道人图 → 确定性路由 → 混合导演 → 可恢复）。整个编排包最终约 1900 行 Python，14 个文件。&lt;/p&gt;&#10;&lt;p&gt;验收有一条硬场景，就是开头那条报数：四个成员各回自己的序号，人设只能加语气，不能改数、不能漏人、不能加戏。这条过不了，迁移就不算完。&lt;/p&gt;&#10;&lt;h2 id="复盘"&gt;复盘&#10;&lt;/h2&gt;&lt;ul&gt;&#10;&lt;li&gt;分层不是按语言划的，是按职责性质划的。数据是稳定的，编排是高频变化的。把变化最快的职责放进迭代最快的生态：LangGraph 的图结构、checkpointer、子图、结构化输出校验都是现成的，Go 里全要手写。&lt;/li&gt;&#10;&lt;li&gt;图的节点边界就是恢复边界。LangGraph 的检查点落在节点边界，所以节点怎么切，决定能从哪里恢复。想要发言人粒度的恢复，就得把调度切成发言人粒度的节点。&lt;/li&gt;&#10;&lt;li&gt;确定性与概率性分离，是 Agent 系统的地基。能写成代码的绝不调模型；需要模型的环节（Supervisor）只出可校验的结构化计划，失败可回退。开头那个报数 bug，本质就是没有这层分离。&lt;/li&gt;&#10;&lt;li&gt;契约先行让两端各自演进。11 个类型化事件 + run_id 幂等 + 快照式输入，Go 和 Python 之间只有契约没有纠缠；Go 仍然是数据的事实源，Python 在快照上做编排，记忆用提案-校验的方式回流。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;这次重构真正的收益在后面：工具调用、人工审批中断（&lt;code&gt;permission_required&lt;/code&gt; 事件已在契约里预留）、长任务规划、更复杂的多 Agent 协作，这些以后都是&amp;quot;在图里加节点&amp;quot;，不再需要动 Go。架构分层的意义不是当下的优雅，是把未来的变化留在了便宜的地方。&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;封面图：&lt;a class="link" href="https://www.flickr.com/photos/Creativity103/5227842611" target="_blank" rel="noopener"&#10; &gt;Creativity103 / Flickr&lt;/a&gt; · CC BY 2.0&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;</description></item></channel></rss>