☰
拆解11个编码智能体:为何弃用LangChain与向量检索?
2026/10/22 9:26:18 网站建设 项目流程

最近我干了一件很费时间但特别有意思的事:把市面上能搞到的 11 个主流 coding agent 全拆了一遍,包括两大模型生态官方出的旗舰编码助手、几个开源社区的替代品、还有 IDE 官方插件和第三方插件。所谓“拆”,不是对着文档读,而是真把它们跑起来,挂上本地调试代理看每个请求长什么样,工具调用怎么组织,会话状态在哪一步持久化,甚至直接翻安装包里缓存的依赖树。拆完以后,两个结论非常反常识:这 11 个产品里,没有一个真正用 LangChain 这类编排框架充当主循环;同时,也没有一个用向量检索/Embedding 召回的方式来理解代码库。这篇把我是怎么拆的、拆到了什么,以及自己重写一个极简 agent 内核的过程,一起讲清楚。

1. 拆了 11 个编码 Agent 之后,先看两份反常识的解剖结果

1.1 我是怎么拆的:把每个请求、每个依赖都过了一遍

先说方法论,免得你觉得我在口嗨。拆一个 coding agent,光读源码没用,因为商业产品不会把核心逻辑开源,开源项目你又容易陷入“看代码细节”的局部视角。我的做法分三层:

第一层,行为观察。准备一个干净的沙盒环境,里面放一个有一定规模但不至于太大的模拟项目,比如一个带前端、后端、测试用例的 TODO 应用,然后给每个 agent 下同一条任务:让它在项目里新增一个接口并接上测试。整个过程用本地 HTTP 调试代理把流量记录下来,重点看 LLM 请求体里的 messages 结构、工具调用字段、以及每轮之后模型收到的工具结果长什么样。这一步能看出很多东西:到底是谁在控制循环,上下文是每次都全量重发还是做了增量改写,工具结果有没有被截断或摘要化。

第二层,依赖审计。把发布包拉下来,npm install 之后直接看 lockfile,统计依赖树里有没有编排框架、向量数据库客户端、Embedding 模型 SDK 之类的库。这一步非常直观,有没有用 LangChain、有没有接向量检索,看依赖树基本就一目了然。

第三层,黑盒反推。通过修改工具返回内容、断网、插入非法指令等办法,观察 agent 会怎么反应,从而反推出它的状态机设计和错误处理路径。比如故意让测试命令返回超时,看它是会重试、直接改代码,还是会向用户承认失败。这些行为特征会暴露底层架构的真实复杂度。

三层做完,结论基本就清楚了。

1.2 第一份反常识:编排框架不在任何一条主链路上

先回答最扎眼的问题:这些产品到底用不用 LangChain?我的结论是,作为辅助工具可能有人整合过向量化方案或者做实验性功能时会参考,但没有任何一个把 LangChain 放进核心的 agent loop。什么叫核心的 agent loop?就是决定“模型 → 工具结果 → 模型”这个循环怎么转、谁来转的那部分逻辑。

从请求日志就能看出来。用 LangChain 的 agent 会有非常明显的模板特征:系统提示词里会带着框架固定格式的 ReAct 指令,tool_calls 的组装方式也是框架统一封装的。但我在这些产品里看到的完全不是这样:它们每一个都有自己的系统提示词、自己的状态转换逻辑、自己的工具执行器和自己的流式协议。工具的命名、消息的格式、重试的策略都各不相同,说明这层全是手写的。

更有意思的是,其中不少产品本身还提供了一个“路由层”,比如先探测仓库类型,再决定由哪组技能包来处理。这类逻辑用编排框架写会非常别扭,因为框架喜欢把流程固化,而 coding agent 的流程是高度动态的:模型下一轮要调用什么工具的决策权在模型手里,不在编排器手里。

1.3 第二份反常识:代码检索不靠向量召回

接着是第二个反常识结论:没有一个产品把向量检索/Embedding 召回作为读代码的主路径。注意,不是说它们完全没有语义搜索能力,有些产品确实提供“代码语义搜索”之类的功能,但回归到 agent 实际执行任务时,定位代码文件、找函数定义、查引用列表这些操作,走的全是另一套工具链:文本搜索、AST 解析、符号索引、语言服务协议。

最典型的现象是,这些 agent 的工具列表里出现的不是“向量搜索代码库”,而是“grep 代码”“查找符号”“读取文件目录”“获取文件大纲”“查看最近修改历史”这类非常朴素的工具。它们的共同特征是精准、快速、可解释,而不是“相似度排序”。

为什么生产级产品集体做了这个选择?这才是值得展开的地方。我下面两章分别把 LangChain 和向量检索这两件事拆透。

2. 为什么他们集体抛弃了编排框架:抽象层越厚,越没有掌控感

2.1 编排框架做了太多你不需要的“智能”

LangChain 这类框架的核心假设是:把你的任务抽象成一条 chain,或者一个有向图,框架负责路由、记忆、工具选择,你只需要配置各个节点。这个抽象在聊天机器人、文档问答这类场景里很受用,但在 coding agent 里,它首先侵犯了一个底线:你完全失去了对“当前到底进行到哪一步”的精确控制。

编码任务里模型的决策经常是概率性的。上一秒它觉得要读 A 文件,读完之后发现不对,下一秒就转向 B 文件;再下一轮它可能连续调用三个工具。这种高熵行为流里,你必须有硬编码的约束来兜底,比如最大迭代次数、单次工具执行时间、权限校验、会话快照。这些约束不是靠“链”能表达的,因为它们不是流程的下一步,而是无限循环里的不变量。

更重要的是流式体验。生产级 agent 的用户界面里,你要实时看到模型输出的每一个 token、当前正在运行什么工具、工具执行进度,甚至还可以随时打断改指令。要实现这种体验,你需要控制底层传输层,自己做 SSE 协议,自己定义事件类型。而框架通常把流式封装成高层抽象,你想在中间插入一个自定义事件,就得跟框架的 callback 机制搏斗。我实际见过有人在 LangChain 的流式接口上打了三个补丁都搞不定 token 级推送,最后愤而重写循环。

2.2 生产级 agent 的实际路径:原生工具调用加极薄封装

那不用框架,这些产品用什么?答案是模型厂商的原生工具调用能力,加一层极薄的封装。

现在各家大模型 API 基本都提供 function calling / tool use 接口,请求体里带上工具描述,模型返回两类内容:要么是直接文本,要么是结构化工具调用指令。Agent 循环的本质就是做三件事:把聊天记录连同工具定义发给模型;模型说要调用工具,就执行并把结果以 tool 消息塞回去;模型给出文本,就判定任务结束。

这套循环的代码量甚至可以压到几十行。我自己的模拟项目里就写过一版,核心逻辑不到 80 行。真正的复杂度不在循环本身,而在外围:工具定义怎么设计才不会让模型眼花缭乱,工具结果太长怎么压缩,会话怎么持久化,权限怎么控制,模型跑飞了怎么强制停止。这些外围问题恰恰是框架很难帮你收敛的,因为每个产品面临的工具集、用户习惯和交互方式都不一样。

我看到的现象也是这样:各产品的主循环都不复杂,差距全在外围。比如有的产品会在工具执行前加一层“命令审核”,有的会在会话里维护一个“文件系统快照”,有的会做工具失败后的自动降级。这些策略如果硬套进框架的 agent 概念里,写起来会非常别扭。

2.3 所谓的高级编排,最后都收敛成状态机

把很多产品内部的状态流转画成图,你会发现它根本不像 LangChain 里那种“链式流水线”,更像一个有限状态机。常见的状态大概是:

等待用户指令 → 规划任务 → 执行工具 → 观察结果 → 调整计划 → 继续执行,直到完成或放弃。

状态之间怎么转移?一部分由模型决策驱动,比如模型说“我要调用测试工具”,就进入执行状态;但另一部分由不变式强制驱动,比如连续十次工具调用都没有产生文件变更,强制回到“询问用户”状态,否则这个 agent 可能在一个死循环里烧掉几百次请求。

为什么不把这层交给框架的 agent 概念?因为框架的 agent 更关注“怎么组合工具”,而 coding agent 更关注“怎么维持状态”。你真正需要的不是一长串预先编排的链,而是能随时响应模型新决策的循环,并且在循环的每个出口都放上护栏。状态机的好处是你可以清楚地回答任何时刻的问题:现在在哪一步、下一步可能去哪、什么条件下会回退。这个清晰度直接影响调试体验,而调试一个 agent 恰恰是产品化过程中最花时间的环节。

3. 为什么没有人用向量检索读代码:Agent 找的不是“相似”,是“落点”

3.1 传统 RAG 的教科书假设,在编码场景里全塌了

讲课式的 RAG 流程是:把文档切成 chunk,用 Embedding 转成向量存进向量库,用户提问时算相似度取 TopK,拼进上下文。这套流程有三个隐含假设:

第一,语料相对静态,可以离线索引;第二,查询和文档之间的相关性可以用语义相似度量出来;第三,TopK 里的内容即使不是完全精确,也能帮模型“猜出个大概”。

编码任务里这三个假设几乎全不成立。代码库是高频变动的,改一行代码就会让函数签名变化,离线向量索引很容易过期;代码里的核心操作是确定性引用而不是语义相似,比如“找到某配置项在哪个文件里被赋值”这种问题,向量检索帮不上忙;更重要的是,编码 agent 要做的不是“猜个大概”,而是要精确定位一个符号、读取一个文件的全部内容、修改一段代码块,任何“差不多的结果”都需要额外的时间去验证,反而拖慢节奏。

拿一个很常见的需求打比方:用户让 agent 把项目中某个函数从同步改成异步。正确的做法是先定位函数定义位置,找所有调用点,逐个判断调用处是否需要加 await。这套流程里,最有价值的检索是“找定义”“查引用”“看文件”,而不是“找一段看起来像的代码”。用向量检索很容易给你返回七八个结构类似的函数,但你要的只是那一个。

3.2 Agent 实际浏览代码库的方式:先把检索看成机械动作

我在第 1 章说这些 agent 用的都是很朴素的工具,那它们到底怎么在代码库里找东西?还原一下真实过程。

接到任务后,第一步通常是扫描仓库结构,看入口文件、配置文件、目录分布,建立基本认知。第二步会用文本搜索或者目录模糊匹配去定位关键文件,比如找一个包含特定环境变量名的配置。第三步打开目标文件,先看整体结构再定位具体行。如果需要跨文件理解,就查符号定义和调用关系。整个过程中,LLM 负责从零散的读操作里理解上下文,工具层只负责把“找”这个动作做到极致地精确。

仔细琢磨一下会发现,agent 在代码库里的行为跟人非常像:人会先 grep 再打开文件,而不是先让“语义搜索引擎”把最相关的片段推给自己。这也是为什么那些开源方案里的检索工具通常是 grep、ast、LSP 这类东西,而不是向量数据库。它们追求的是毫秒级返回精确结果,让模型的推理不被噪声干扰。

我整理过一张对比表,可以更直观地看到两种方案在编码场景里的差异:

对比维度向量检索/Embedding 召回文本检索/符号索引/AST 定位
定位精度模糊,返回相似片段精确到文件、符号、行号
可解释性低,无法说明为什么返回这段高,每条结果都有明确依据
延迟较高,涉及向量化与相似度计算低,通常毫秒级
索引维护需要定期重建或增量更新跟随文件系统实时变化
对模型推理的负担易引入噪声,需要模型二次筛选信息密度高,直接作为依据

这个表不是说向量检索没有价值,而是说在编码 agent 里,它不属于“读代码”这条主链路。

3.3 语义理解到底被藏到了哪里?

一个自然的问题是:那“语义理解”是不是就不需要了?完全不是,语义理解没有消失,而是被提到了更高层——交给了 LLM 本身。

模型本身就能阅读代码、理解意图,它缺的不是“语义能力”,而是“精确定位的机械能力”。所以架构上最好的分工是:LLM 负责看懂,工具负责找到。如果工具层再叠加一个 Embedding 排序,等于在工具和模型之间多造了一个“小语义模型”,既增加了延迟和成本,又容易因为向量化误差导致错误召回。

而且从实际效果看,当模型面对一份被 TopK 拼接出来的“相似代码片段”时,它常常会误以为这就是需要修改的目标,导致改错地方。这种幻觉成本在编码任务里非常高,因为你不仅要多花一轮请求去纠正,还可能产生无意义的 diff。相比之下,文本搜索结果天然带文件路径和行号,模型不太会产生位置幻觉。这也是生产级产品取舍时最重要的考量。

4. 一个能跑起来的极简内核:我的“五十行 agent 核心循环”

4.1 先看这个主循环:它比我预想中更简单

看完那些产品之后,我自己也动手写了一个模拟项目 X,目标不是做一个完整产品,而是验证“一个极薄的循环到底能不能撑住复杂编码任务”。核心逻辑长这样:

async def run_agent(session): max_iterations = 30 while session.state not in ("done", "failed", "ask_user"): if session.iteration >= max_iterations: session.state = "ask_user" break response = await session.llm.step( messages=session.messages, tools=session.enabled_tools(), ) # 模型给出最终文本,任务结束 if response.output_text and not response.tool_calls: await session.stream_text(response.output_text) session.state = "done" continue # 模型要求调用工具,逐个执行 for call in response.tool_calls: result = await session.dispatch(call) session.messages.append({ "role": "tool", "tool_call_id": call.id, "content": truncate_result(result), }) session.iteration += 1 # 每轮结束后持久化,支持崩溃后续跑 await session.save_snapshot()

这段代码看起来简单得不像生产级方案,但它完美复刻了我看到的那些产品的核心骨架。每次循环都把完整的消息历史发给模型,模型负责决定下一步;工具执行完把结果塞回消息列表;循环继续。模型说“完成”或者迭代次数耗尽,循环结束。

这里有几个隐含决策值得解释。为什么不把已执行的结果从消息里去掉,而是每次全量重发?因为模型的上下文是连续的,它需要看到自己前面做过什么。为什么不直接把工具结果原样塞回去?因为结果可能太长,必须截断。为什么每轮之后都要持久化?因为编码任务往往需要跑几十轮,中途崩溃如果从零开始,损失太大。

4.2 核心循环虽然短,扩展点才是真正的战场

真正让人意识到“不用框架是对的”的时刻,不是写循环的时候,而是往循环里加扩展点的时候。

我在第一版里只支持文本工具,后来要接入命令执行、文件读写、浏览器调试工具,于是加了一个动态工具注册表。每个工具的参数用 JSON Schema 描述,模型调用时按 schema 传参。这个注册表看起来简单,但设计时要回答一系列问题:工具参数校验失败,是重试还是直接报错?工具超时了,是杀掉还是继续等?工具结果包含敏感信息,怎么在返回给模型前脱敏?这些问题没有一个与“agent 循环”直接相关,但每一个都会决定这个 agent 在生产环境里能不能用。

我还有一个体会:工具返回的错误信息比工具本身的重要程度高得多。模型调用工具失败后,能不能从错误消息里理解下一步该怎么做,直接决定它能不能自行纠偏。很多工具的报错信息是给人类看的,充满嵌套堆栈,模型看到之后毫无头绪。我在模拟项目里花了很多精力做“错误信息重写”,把一段乱糟糟的报错压缩成三行可操作的描述。这层处理放在框架里很难优雅地表达,但在你自己的循环里,它可以只是一段普通函数。

4.3 给极简内核加上上下文压缩与断点续跑

循环一旦跑久了,消息历史一定会膨胀。文件内容、命令输出、错误堆栈会快速占满上下文窗口。我的方案是分层压缩:

第一层,结果截断。单条工具结果保留最多两三百行,超出部分丢弃,同时在消息里注明“结果已被截断,原始内容长度是多少”。这个标注很重要,模型至少知道自己看到的是不完整的,不会产生“这个列表只有十条”的错误结论。

第二层,摘要替换。当某条工具结果在后续推理中不再直接需要时,用一个 LLM 生成的摘要替换原来的详细内容。比如“测试运行失败,失败用例集中在登录模块,详情已省略”,既保住关键信息又控制体积。

第三层,增量编辑。某些模型服务端支持局部替换消息内容,不用每次都全量重发历史。如果平台不支持,就在本地维护一个压缩后的消息列表,把早期的大段内容包括进一个不断更新的总结消息里。

断点续跑也按同样的思路设计:把会话序列化成 JSON 落盘,内容包括消息列表、当前状态、已执行工具次数。每次模型调用前和调用后都存一次。这样即便进程被杀,重启后也能把状态恢复出来,而不是每次都要跟用户重新对齐进度。这个能力在生产环境里非常重要,因为长任务一定会遇到断网、超时、进程崩溃。

5. 生产环境踩坑录:这 11 个产品不会写进文档里的细节

5.1 工具定义越加越多,模型反而越笨

这是我在模拟项目里踩得最深的一个坑。一开始我给 agent 配了三十多个工具,心想工具越多,模型能力越强。结果测试下来,模型在每一步选择工具时都变慢了,而且经常选错。原因很简单:模型在超长工具列表里做选择,本质上就是一次大规模分类任务,选项越多,注意力越分散。

后来参考那些拆出来的产品的做法,改成二层调度:先由调度模型决定“当前任务属于哪个子能力域”,再由域内模型在该域的工具列表里做选择。比如用户说“帮我跑一下测试并修复失败”,调度模型先判定进入“测试与调试域”,然后只把测试执行、日志查看、错误定位这几个工具暴露给执行模型。这一下就把工具选择范围缩小到一个可管理的规模。

还有一个变种是动态工具生成:不维护固定的大列表,而是根据当前任务实时调整工具描述。比如当模型在改一个 Python 文件时,才把 Python 语法检查工具注册进来;当它在改前端代码时才注册浏览器调试工具。工具描述也可以随对话进展变得更具体,比如带上前置条件“必须在文件保存后才能运行”。

5.2 上下文爆炸:不是删旧消息,而是做分层记忆

编码任务跑得越久,上下文里堆得最多的往往不是代码本身,而是中间产物:日志、报错、测试输出、临时修改记录。这些东西一次性全塞回去,模型很快会“眼花缭乱”,开始漏看关键信息。

我试过的不靠谱方案是直接截断旧消息。问题是模型可能会遗忘它自己之前做的决策,后面再问它“你为什么改了那个文件”,它一脸茫然。更好的方式是分层记忆:原始工具结果超长就压缩成摘要;分析结论单独存下来,在后续每轮都固定注入;文件最终状态放到一个专门的“当前项目状态”模块里,模型需要时再读取。这三层加在一起,能让模型既保留决策依据,又不被海量细节淹没。

具体来说,我会在每次工具结果返回后快速判断它的“保鲜期”。日志这类一次性的内容,立即做摘要;文件读取这类可能有后续修改的内容,保留完整但限制条数;测试报告这类需要对比上次结果的内容,存成结构化数据,只把差异部分发给模型。

5.3 最大的坑:工具结果太长,直接把模型“喂瞎”了

这是个听起来很基础、但实际影响极大的问题。工具返回一个大文件或者一整页 log,如果直接塞回上下文,模型在下一步推理时注意力会被那些无关紧要的行分散。测试下来会发现,同样的任务,把返回结果从几千行压缩到几十行摘要之后,成功率和执行速度都明显提升。

压缩不是简单按长度裁,而是按结构保留。日志类结果只保留报错行、异常栈顶、统计汇总;diff 类结果保留 hunk 头、新增行、删除行附近的上下文;目录列表保留文件路径和最后修改时间;测试输出保留失败用例名称、期望值和实际值。这需要为每种工具单独写一个压缩器,但其实逻辑都不复杂,只是需要一点耐心。

另外我强烈建议给每条工具结果带上 is_truncated 标志。模型看到“这个列表有 500 项,但我只显示前 20 项”时,通常能正确提出“我需要更多细节”的后续请求。如果它以为列表就 20 项,就会基于错误前提继续行动。

5.4 常见问题速查表

我按实际调试记录整理了一张问题表,对排查同类问题很有帮助:

现象可能原因排查方式解决参考
模型反复调用同一个工具工具结果不明确或未产生预期变更检查工具返回内容是否包含充足反馈在结果里增加“当前状态”“已生效变更”字段
模型做出违规操作权限控制只在 UI 层,不在循环内检查工具执行前是否有独立校验把权限判断下沉到 dispatch 阶段
会话越跑越慢消息列表里累积了过多历史查看每次请求的 token 消耗启用摘要替换和局部编辑
修改文件后模型仍使用旧内容模型上下文中的文件内容未更新检查文件读取工具是否有缓存在文件写入工具执行后主动刷新相关缓存
模型输出结构不稳定工具参数的 JSON Schema 过宽逐条校验工具参数用校验器捕获并返回重试消息
长任务中途崩溃丢失进度会话没有持久化检查每次调用前后的落盘逻辑增加快照机制,支持断点恢复

这张表没有高深的理论,但每一条都是真实调出来的血泪教训。

6. 如果你想自己做 coding agent,我的下一步建议

6.1 先别急着选框架,先定“形态”

很多人一上来就问该用 LangChain 还是自己写,这个问题问早了。先想清楚你要做的东西是什么形态。命令行工具、IDE 插件、网页端应用、后台服务,每个形态的复杂度完全不同。

IDE 插件可以直接依赖语言服务协议,省掉自己写 AST 解析的麻烦;命令行工具最需要权限系统和终端交互能力;网页端则要处理仓库同步、文件树展示、流式输出渲染,还涉及在前端维护会话状态。形态定了,技术选型自然就缩小了。

我个人的判断是,小规模工具和个人项目完全不排斥框架,因为迭代快;但如果你要做的产品需要深度控制交互细节、要在多种模型之间切换、要对每一步行为做审计,那自己维护一个轻量循环的成本远低于你在框架里挣扎的成本。这个分界线大概在你第三次想往框架的抽象夹层里塞自定义逻辑的时候。

6.2 学“接口协议”,别只学“框架 API”

拆完这些产品后,我最大的收获是:它们都把自己的实现建立在协议层,而不是建立在某个框架的 API 上。模型厂商的工具调用格式是协议,SSE 流式事件是协议,工具与模型之间的消息规范也是协议。

把协议层吃透,你就获得了最大程度的可移植性。今天接这家模型,明天换那家模型,只要把请求体从一种格式转成另一种格式,上层逻辑完全不用动。这也是为什么这些产品可以快速跟进各家新模型,因为它们不是围绕某个 SDK 构建的,而是围绕消息规范构建的。

我还建议关注模型服务端的一些新接口,比如上下文局部编辑能力,允许只替换消息列表中的某一段内容而不是全部重发。这类接口会给 agent 的上下文管理带来很大优化空间,值得提前研究。

6.3 下一步值得重点研究的三件事

一是工具与生态的标准化。现在业界在推统一的工具调用协议,让同一个工具定义可以被不同模型使用。这个方向如果成熟,agent 的工具生态会迎来一次大爆发。二是模型路由器。不是每个决策都要用最强最贵的模型,很多工具调用不值得动用旗舰模型,路由层根据任务难度做分配,成本会显著下降。三是本地优先的检索能力。这里不是说向量检索,而是把仓库解析、符号索引、变更检测都放在本机完成,只在需要模型推理时才请求云服务,既保护隐私又降低延迟。

这三个方向其实已经出现在那些头部产品的路线图里,只是从外部看不太明显。如果你想长期做 coding agent,这几块值得提前布局。

最后再分享一点我个人的体会:写完那个 80 行的极简循环之后,我一度觉得 coding agent 也不过如此,但真正把它接到真实项目上,才发现九成的时间都花在边界情况上——工具崩溃、模型重复调用、超时重试、用户中途改需求。那些复杂的框架解决不了这些边界问题,能解决它们的只有足够细致的状态设计与错误处理。所以如果你的目标是做一个真正能落地的 coding agent,不用纠结要不要用 LangChain,也不要在向量检索上折腾太久。把主循环写得足够简单,然后把精力放到工具结果的质量和上下文的健康度上。这两个地方做好了,即使你的内核只有几十行,它也能跑出比很多花哨架构更稳的效果。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询