最近我干了一件很费时间但特别有意思的事把市面上能搞到的 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( messagessession.messages, toolssession.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也不要在向量检索上折腾太久。把主循环写得足够简单然后把精力放到工具结果的质量和上下文的健康度上。这两个地方做好了即使你的内核只有几十行它也能跑出比很多花哨架构更稳的效果。