1. 生产级改造
上一篇把「获取地址 → 获取天气」放在同一条并行分支里。它能说明依赖关系,却默认定位服务和天气服务永远成功。真实环境会遇到定位权限被拒绝、地址格式不一致、主天气源超时、返回数据过期,以及用户在请求结束前关闭页面等情况。
如果继续把这些判断塞进一个 requestWeather 函数,函数很快就会变成一长串 try/catch。更麻烦的是,父流程只能看到「成功」或「失败」,看不到失败发生在哪一步,也很难从中间位置恢复。
生产级改造的目标不是增加更多代码,而是让每一种变化都有明确归属:
| 变化 | 应该由谁处理 | 父流程需要知道吗 |
|---|---|---|
| 定位失败与地址标准化 | 天气领域流程 | 不需要知道细节 |
| 主源超时与有限重试 | 天气领域流程 | 不需要知道次数 |
| 数据过期与备用源 | 天气领域流程 | 只需要最终来源 |
| 用户取消请求 | 整条运行链路 | 需要立即停止 |
| 最终天气是否可信 | 天气领域输出契约 | 需要知道结果 |
2. 子图
LangGraph 的子图就是一张可以作为父图节点使用的编译图。父图把「查询可信天气」看成一个节点,子图内部再展开定位、标准化、主源查询、质量校验、重试和降级。
这张图要关注两个边界。虚线框外是父图,它只传入原始问题并接收可信天气;虚线框内是天气团队自己维护的领域流程。主源重试了几次、为什么切换备用源,都不应该变成父图的分支。
1const weatherSubgraph = createProductionWeatherSubgraph()23const parentGraph = new StateGraph(WeatherState)4.addNode('weatherSubgraph', (state, config) => (5weatherSubgraph.invoke(state, config)6))7.addEdge(START, 'weatherSubgraph')8.addEdge('weatherSubgraph', END)9.compile()
这里父图和子图使用相同的状态字段,所以编译后的子图可以直接作为节点。大型项目也可以让两者使用不同状态,再在节点边界显式映射输入和输出。
3. 状态
子图中的 WeatherState 记录输入、标准地址、查询次数、天气数据、质量结论和最终摘要。它们不是为了把所有变量堆进一个对象,而是为了让条件边拥有可检查的事实。这里使用 Annotation.Root 定义 LangGraph 状态通道;需要校验外部输入时,仍在工具边界使用 Zod。
1export const WeatherState = Annotation.Root({2input: Annotation<string>,3city: Annotation<string>,4providerAttempts: Annotation<number>,5quality: Annotation<'unknown' | 'invalid' | 'valid'>,6lastError: Annotation<string>,7summary: Annotation<string>,8})
例如,providerAttempts 决定还能不能重试,quality 决定是否可以结束,lastError 则帮助日志和界面解释为什么进入下一条边。节点只返回自己修改的字段,LangGraph 负责把更新合并回状态。
状态字段越多,Agent 的复杂度也越高。每个字段都意味着额外的来源、更新时机和一致性规则。因此只保存后续节点真正需要判断或追踪的数据,不要把每个函数的局部变量都升级成图状态。
4. 重试、校验和降级
主天气源返回后,流程不会立刻相信结果,而是进入质量校验节点。条件边根据状态做三选一:结果可信就结束;尝试次数未到上限就重试;次数耗尽则切换备用源。
1function routeAfterQualityCheck(state: WeatherGraphState) {2if (state.quality === 'valid') return 'finalize'3if (state.providerAttempts < 2) return 'queryPrimary'4return 'queryBackup'5}
「有限」很重要。如果没有 providerAttempts < 2,临时错误可能让图无限循环。生产系统还会为每个请求设置超时、为整张图设置 recursionLimit,并把浏览器的 AbortSignal 一路传到最底层请求。
备用源也必须经过相同的质量校验,而不是因为它叫「备用」就默认可信。这样,所有供应商最终都遵守同一份输出契约。