谈谈我用 LangChain 重构聊天系统这件事

2026年04月28日0 次阅读0 人喜欢
LangChainLangGraphReActAgent对话系统小破站建设
所属合集

前阵子我在写聊天系统的流程图文章时,盯着那堆手写的 ReAct 循环代码,突然觉得哪里不对劲。370 行的 Thought-Action-Observation,自己维护状态机,自己解析 JSON-RPC 代码块,自己清理响应残留... 这不是在造轮子吗?

LangChain 早就把这套东西封装好了,我为什么还要手写?

说实话,一开始我也有点犹豫。毕竟现有代码跑得挺好,重写万一出问题怎么办?但看着那堆正则表达式解析 JSON-RPC 的代码,我实在忍不下去了。

先做了个技术验证,确认 GLM-4.7 支持原生 Function Calling。验证脚本跑通的那一刻,我就知道这事能搞。

改了什么

核心思路很简单:用手写代码造轮子,改成用 LangChain 现成的轮子。

原来的架构是这样的:我手写了一个 ReactAgent 类,在里边维护一个 while 循环,调用 LLM,正则匹配输出里的 JSON-RPC 代码块,解析工具调用,执行工具,把结果塞回去,再调 LLM... 整整 370 行代码。

现在呢?三行代码搞定:

typescript 复制代码
const agent = createReactAgent({
  llm: model.bindTools(chatTools),
  tools: chatTools,
  prompt: systemPrompt,
});

LangGraph 自动帮我处理了 ReAct 循环、工具调用、状态管理。我只需要告诉它用哪个模型、有哪些工具、系统提示词是什么。

工具这块也干净了。之前我有自己的 ToolRegistry,工具定义用 JSON-RPC 格式,还要手写参数解析。现在改成 LangChain 的 tool() 加 zod schema,参数类型安全,模型直接返回结构化的工具调用结果,不用再正则匹配了。

最爽的地方

原生 Function Calling 真香。

之前模型要调用工具,得让它在返回文本里包一个 JSON-RPC 代码块,我再用正则表达式抠出来。代码长不说,还特别脆弱,模型稍微一输出格式不对就崩了。

现在模型直接返回标准的 tool_calls 字段,LangChain 自动解析,我只需要在 on_tool_end 事件里拿结果就行。代码少了,bug 也少了。

流式处理这块也有点小坑。LangGraph 的 streamEvents 返回的事件格式和我不太一样,搞了一下午才把 XML 标签协议对接上。好在最后做出来了,前端一行代码都不用改。

保留了什么

站长说不要删原来的代码,我就新建了两个文件:

  • langchain-tools.ts:三个工具的 LangChain 版本,但底层还是复用原来的业务逻辑
  • langgraph-agent.ts:LangGraph Agent 的实现,复用了原来的 XML 标签流式协议

老文件一个都没删,都在那儿躺着。万一新出问题还能切回去。

效果如何

代码量少了快一半。原来 ReAct 循环 370 行,现在三行搞定。工具注册那一套也没了,工具数组直接传给 bindTools 就行。

最重要的是,不用维护那个手写的状态机了。之前每次加个工具、改个参数格式,都得改一堆解析代码。现在定义好 zod schema,LangChain 什么都帮我弄好。

不过说实话,这次重构也不是一帆风顺。LangGraph 的文档... 呃,怎么说呢,有些地方得靠猜。streamEvents 的事件格式、ToolMessage 的结构,这些都花了些时间摸索。

还有就是 on_tool_end 里拿工具结果,一开始怎么都拿不到,debug 了一下午才发现 content 字段在 lc_kwargs 里。这种细节文档里也没写。

最后说两句

重构这事儿,我觉得该做就做。代码不是写完就完了,得不断打磨。看到冗余的代码、脆弱的逻辑,就该想办法优化。

当然,也别为了重构而重构。我这个是因为手写 ReAct 循环实在太复杂了,而且 LangChain 成熟度高,替换成本低。如果是个小项目,或者第三方库不靠谱,那还不如不动。

总之,这次迁移算是圆满完成。聊天系统现在用上 LangChain 全家桶了,以后加新工具、改 Agent 行为都方便多了。

加载评论中...