1. 拆开 11 个 coding agent 之后,我看到的真实技术底牌
前阵子我把市面上能拿到的 coding agent 挨个拆了一遍,前前后后一共 11 个,包括 Claude Code、Codex 这类大家天天在用的,也有一些小众但设计思路很有意思的。拆完之后最大的感受不是“哇好复杂”,而是“原来这么朴素”。这些 agent 没有一个用 LangChain 这类编排框架,也没有一个用向量检索来索引代码库。这个结论刚出来的时候我自己都愣了一下,因为过去两年整个行业都在讲 RAG、讲向量数据库、讲 agent 框架,结果真正把产品做出来、做到能用的团队,反而把这些东西全绕开了。
这篇文章我想干的事很直接:把这 11 个 coding agent 的共性设计拆给你看,讲清楚它们为什么不用 LangChain、为什么不用向量检索,那它们到底用什么来理解代码、定位文件、执行任务。我会把核心思路、关键实现、参数取舍、踩坑经验都摊开讲,让你看完之后能自己动手复现一个最小可用的 coding agent,而不是停留在“调个框架 API”的层面。
适合谁看?如果你正在做 AI 编程工具、想理解 agent 的真实工程实现、或者单纯好奇 Claude Code 这类产品内部是怎么跑的,这篇都值得你花时间。如果你只是想找个框架套壳,那可能会有点失望,因为接下来讲的东西,大部分是“反框架”的。
先说结论,省得你往下翻:这 11 个 agent 的共性可以概括成三句话——用文件系统当记忆,用 grep 和 glob 当检索,用 shell 当执行器。听起来是不是特别土?但就是这套土办法,撑起了目前最好用的编程 agent。
2. 为什么大家都不用 LangChain 和向量检索
2.1 LangChain 的问题:抽象层太厚,调试成本太高
我先说说 LangChain 为什么在这些项目里集体缺席。LangChain 的核心价值是提供一套统一的抽象,把 LLM 调用、工具、记忆、链式编排都封装起来。听起来很美,但实际用起来,尤其是做 coding agent 这种需要精细控制每一步的场景,问题就暴露了。
第一个问题是抽象泄漏。LangChain 的 Chain、Agent、Tool 这些概念,在简单场景下很顺手,但一旦你要控制“模型先看哪个文件、看完之后决定下一步干什么、失败了怎么回退”这种细粒度逻辑,就会发现框架的抽象和你的需求对不上。你要么绕过框架直接调底层 API,要么写一堆自定义组件把框架的抽象再拆开,两头不讨好。
第二个问题是调试困难。coding agent 的运行过程是一长串的模型调用加工具调用,中间任何一步出错都可能导致结果跑偏。LangChain 把很多细节藏在内部,出错的时候你看到的是一层层包装过的异常,定位问题要翻好几层源码。而这些 agent 的开发者普遍反映,他们需要的是“每一步都看得见、改得动”,而不是被框架托管。
第三个问题是版本迭代快、破坏性变更多。做 coding agent 的团队往往需要长期维护,框架的 API 一变,整个项目就得跟着改。相比之下,直接调模型厂商的 SDK,虽然代码写得多一点,但胜在稳定可控。
提示:这不是说 LangChain 不好,而是它的定位和 coding agent 的需求不匹配。做简单 demo 用框架很爽,做需要精细控制的生产级 agent,直接写反而更省心。
2.2 向量检索代码的致命缺陷:粒度对不上
再说向量检索。过去大家做代码问答,第一反应就是把代码切块、embedding、存向量库、检索。但这 11 个 agent 没有一个这么干,原因很实在。
代码和自然语言文档不一样。一段代码的含义高度依赖上下文,你把一个函数切成 500 token 的块去 embedding,检索出来的往往是“看起来相似但实际不相关”的片段。比如你搜“用户认证”,向量检索可能给你返回一堆名字里带 auth 的函数,但真正需要的那段逻辑可能藏在一个叫handleRequest的函数里,名字完全不沾边。
更关键的是,coding agent 需要的不是“相似片段”,而是“精确的文件和行号”。它要能打开某个文件、读到某一行、然后修改它。向量检索返回的是一堆语义相近的文本块,你还得反推这些块来自哪个文件的哪一段,这个映射过程本身就容易出错。
还有一个现实问题:代码库是动态变化的。你刚建好索引,用户改了几行代码,索引就过期了。重新 embedding 整个代码库的成本不低,而且有延迟。对于需要实时响应的 agent 来说,这个延迟不可接受。
所以这些 agent 的选择是:放弃语义检索,改用精确的文本搜索。grep 找关键词,glob 找文件路径,配合模型自己的理解能力,效果反而更好。因为模型本身已经足够聪明,你给它准确的搜索结果,它能自己判断哪些相关、哪些不相关。
2.3 那它们到底靠什么理解代码
答案可能让你意外:靠模型自己的上下文窗口,加上精准的工具调用。
这些 agent 的核心逻辑是:模型不需要预先“理解”整个代码库,它只需要在需要的时候,用工具去查找、读取、修改。就像一个人类程序员,你不需要背下整个项目的代码,你只需要知道怎么用 grep 找函数、怎么用编辑器打开文件、怎么运行测试。
具体来说,它们给模型提供的工具通常包括这几类:
- 文件查找类:glob 按模式匹配文件路径,grep 按内容搜索
- 文件读写类:read 读文件内容,write 写文件,edit 做精确替换
- 执行类:bash 执行 shell 命令,跑测试、跑构建、跑 git 操作
- 辅助类:列目录、查文件信息等
模型拿到这些工具后,会自己规划步骤:先 grep 找到相关文件,再 read 读取内容,分析后 edit 修改,最后 bash 跑测试验证。整个过程不需要向量检索,也不需要复杂的编排框架,就是一个“模型 + 工具循环”。
这个设计的精妙之处在于,它把“理解代码”这件事交给了模型本身,而不是外部的检索系统。模型在读取真实代码的过程中形成理解,而不是靠预先计算的向量相似度。这也是为什么这些 agent 在处理复杂任务时表现更好——它们看到的是真实的、完整的代码上下文,而不是被切碎的片段。
3. 核心架构拆解:一个 coding agent 到底由什么组成
3.1 主循环:模型调用与工具执行的交替
拆开这些 agent,最核心的部分就是一个循环。伪代码大概长这样:
while not done: response = model.call(messages, tools) if response.has_tool_call: result = execute_tool(response.tool_call) messages.append(result) else: done = True看起来简单到不像话,但魔鬼在细节里。这个循环有几个关键设计点。
第一是消息历史的管理。coding agent 跑一个任务可能涉及几十次模型调用,消息历史会越来越长。如果不加控制,很快就会超出上下文窗口。这些 agent 的处理方式通常是:保留完整的工具调用记录,但对工具返回的大块内容做截断或摘要。比如读一个 1000 行的文件,不会把全文塞进历史,而是只保留关键部分,或者让模型自己决定读哪一段。
第二是工具调用的错误处理。工具执行失败是常态——文件不存在、命令报错、权限不足。好的 agent 不会因为一次工具失败就崩溃,而是把错误信息返回给模型,让模型自己决定重试还是换方案。这个“错误也是信息”的设计,是 agent 能自主解决问题的关键。
第三是终止条件。什么时候算任务完成?通常有两个信号:模型不再调用工具(认为任务结束),或者达到了预设的最大轮数。最大轮数是必要的保护,防止模型陷入死循环。
3.2 工具设计:少而精,每个都要能独当一面
这 11 个 agent 的工具集普遍很小,通常不超过 10 个。这和很多人想象中“工具越多越强大”相反。工具少的好处是模型更容易记住每个工具的用途,选择时不容易混淆。
我统计了一下,出现频率最高的工具是这几个:
| 工具 | 用途 | 出现频率 |
|---|---|---|
| read | 读取文件内容 | 11/11 |
| write | 写入文件 | 10/11 |
| edit | 精确替换文本 | 9/11 |
| bash | 执行 shell 命令 | 11/11 |
| grep | 按内容搜索 | 10/11 |
| glob | 按路径模式查找 | 9/11 |
| list | 列目录 | 8/11 |
read 和 bash 是全员必备。read 不用解释,bash 之所以必备,是因为它能覆盖太多场景:跑测试、跑构建、git 操作、文件操作,一个 bash 顶好几个专用工具。
edit 这个工具值得单独说。它和 write 的区别是,write 是覆盖整个文件,edit 是做精确的字符串替换。为什么需要 edit?因为模型改代码时,往往只需要改几行,用 write 重写整个文件既浪费 token 又容易引入意外改动。edit 让模型只输出要改的部分,配合“旧字符串 -> 新字符串”的替换逻辑,精准且高效。
注意:edit 工具的设计有个坑,就是“旧字符串”必须在文件中唯一。如果模型给的旧字符串在文件里出现多次,替换就会出错。成熟的 agent 会要求模型提供足够的上下文来保证唯一性,或者在替换失败时返回错误让模型重试。
3.3 上下文管理:怎么让模型记住该记的
上下文管理是 coding agent 最容易被低估的部分。一个任务跑下来,消息历史可能包含几十次工具调用和返回,轻松超过十万 token。怎么在有限的上下文窗口里保留最关键的信息,直接决定了 agent 能不能完成长任务。
我观察到的策略主要有三种。
第一种是滑动窗口加摘要。保留最近 N 轮完整对话,更早的内容压缩成摘要。摘要通常由模型自己生成,比如“之前已经读取了 A 文件,确认了 B 函数的问题,修改了 C 处代码”。这样既保留了关键信息,又大幅压缩了 token。
第二种是工具结果截断。读文件时,如果文件很长,只返回前若干行或匹配行附近的内容。grep 的结果也做数量限制,比如最多返回 50 条匹配。这样避免单次工具调用就撑爆上下文。
第三种是外部记忆。有些 agent 会把关键信息写到临时文件里,需要时再读回来。这相当于用文件系统当扩展内存。比如模型发现了一个重要的配置项,可以把它记到一个 scratch 文件里,后面需要时再读。
这三种策略往往组合使用。核心思路是:上下文窗口是稀缺资源,每一 token 都要花在刀刃上。
3.4 提示词工程:系统提示才是真正的“框架”
如果说这些 agent 有什么“框架”,那系统提示词就是它们的框架。系统提示里定义了模型的角色、可用工具、工作流程、注意事项。写得好的系统提示,能让模型自主完成复杂任务;写得差,模型就会乱调工具、反复试错。
我拆的这些 agent,系统提示普遍有几个共同点。
第一是明确的工作流程。比如“先理解需求,再查找相关文件,再制定修改计划,再执行,最后验证”。这个流程不是硬编码的,而是通过提示词引导模型遵循。
第二是工具使用的具体指导。不只是列出工具,还告诉模型什么时候用哪个。比如“查找文件用 glob,查找内容用 grep,修改代码用 edit 而不是 write”。
第三是约束和禁忌。比如“不要修改测试文件来让测试通过”、“不要删除看不懂的代码”、“修改前先读取文件确认内容”。这些约束能避免模型做出危险操作。
第四是输出格式要求。比如要求模型在调用工具前先说明意图,这样用户能看到 agent 的思考过程。
系统提示的长度通常不短,几千 token 很常见。但这是值得的,因为它是 agent 行为的“宪法”,写好了能省下大量调试时间。
4. 实操复现:从零搭一个最小可用的 coding agent
4.1 环境准备与依赖选择
说了这么多原理,不如动手搭一个。我下面给的是一个最小可用版本,能读文件、搜代码、改代码、跑命令。你可以基于它扩展。
依赖方面,我建议直接用模型厂商的官方 SDK,不要上框架。以 Python 为例,核心依赖就两个:模型 SDK 和用于文件操作的库(其实标准库就够)。不需要向量数据库,不需要 LangChain,不需要任何编排框架。
pip install openai # 或其他模型厂商的 SDK如果你用的是支持工具调用的模型,SDK 里通常已经封装好了 function calling 的接口。我们直接用它。
项目结构建议这样组织:
coding_agent/ main.py # 主循环 tools.py # 工具定义与实现 prompts.py # 系统提示词 config.py # 配置这个结构简单清晰,每个文件职责单一,方便调试和扩展。
4.2 工具实现:read、write、edit、bash、grep、glob
先实现工具。每个工具就是一个函数,接收参数返回字符串结果。关键是错误处理要做好,任何异常都要捕获并返回可读的错误信息,而不是让程序崩溃。
import os import subprocess import glob as glob_module def tool_read(path, start_line=None, end_line=None): try: with open(path, 'r', encoding='utf-8') as f: lines = f.readlines() if start_line is not None: lines = lines[start_line-1:end_line] # 加行号,方便模型引用 result = ''.join(f"{i+1}: {line}" for i, line in enumerate(lines)) return result[:50000] # 截断保护 except Exception as e: return f"Error reading {path}: {e}" def tool_write(path, content): try: os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, 'w', encoding='utf-8') as f: f.write(content) return f"Written {len(content)} chars to {path}" except Exception as e: return f"Error writing {path}: {e}" def tool_edit(path, old_str, new_str): try: with open(path, 'r', encoding='utf-8') as f: content = f.read() count = content.count(old_str) if count == 0: return f"Error: old_str not found in {path}" if count > 1: return f"Error: old_str appears {count} times, need unique match" content = content.replace(old_str, new_str, 1) with open(path, 'w', encoding='utf-8') as f: f.write(content) return f"Edited {path}" except Exception as e: return f"Error editing {path}: {e}" def tool_bash(command, timeout=30): try: result = subprocess.run( command, shell=True, capture_output=True, text=True, timeout=timeout ) output = result.stdout + result.stderr return output[:20000] if output else "(no output)" except subprocess.TimeoutExpired: return f"Error: command timed out after {timeout}s" except Exception as e: return f"Error: {e}" def tool_grep(pattern, path=".", include=None): try: cmd = f"grep -rn '{pattern}' {path}" if include: cmd += f" --include='{include}'" result = subprocess.run(cmd, shell=True, capture_output=True, text=True) return result.stdout[:20000] or "(no matches)" except Exception as e: return f"Error: {e}" def tool_glob(pattern): try: matches = glob_module.glob(pattern, recursive=True) return '\n'.join(matches[:100]) or "(no matches)" except Exception as e: return f"Error: {e}"这几个工具加起来不到 80 行,但已经能覆盖 coding agent 的绝大部分需求。注意几个细节:read 加了行号,方便模型引用具体位置;edit 检查了唯一性,避免误替换;bash 设了超时,防止卡死;所有输出都做了截断,保护上下文。
4.3 主循环实现:把工具和模型串起来
主循环的逻辑就是前面说的那个 while 循环。关键是把工具定义转换成模型能理解的格式,然后处理模型的工具调用请求。
import json from openai import OpenAI client = OpenAI() TOOLS = [ { "type": "function", "function": { "name": "read", "description": "Read file content with line numbers", "parameters": { "type": "object", "properties": { "path": {"type": "string"}, "start_line": {"type": "integer"}, "end_line": {"type": "integer"} }, "required": ["path"] } } }, # ... 其他工具定义 ] TOOL_MAP = { "read": tool_read, "write": tool_write, "edit": tool_edit, "bash": tool_bash, "grep": tool_grep, "glob": tool_glob, } def run_agent(task, max_turns=50): messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": task} ] for turn in range(max_turns): response = client.chat.completions.create( model="your-model", messages=messages, tools=TOOLS, tool_choice="auto" ) msg = response.choices[0].message messages.append(msg) if not msg.tool_calls: return msg.content for call in msg.tool_calls: fn = TOOL_MAP.get(call.function.name) args = json.loads(call.function.arguments) result = fn(**args) if fn else f"Unknown tool: {call.function.name}" messages.append({ "role": "tool", "tool_call_id": call.id, "content": result }) return "Max turns reached"这个主循环大概 30 行,但已经是一个能跑的 agent 了。你可以给它一个任务,比如“找到项目里所有 TODO 注释并汇总”,它会自己 grep、read、整理结果。
4.4 系统提示词:给 agent 立规矩
系统提示词决定了 agent 的行为质量。我下面给一个精简版,你可以根据需求调整。
SYSTEM_PROMPT = """You are a coding agent. You help users with code tasks by reading, searching, and modifying files. Workflow: 1. Understand the task first 2. Search for relevant files using glob and grep 3. Read files to understand the code 4. Make changes using edit (preferred) or write 5. Verify changes by running tests or commands Tool usage rules: - Use glob to find files by path pattern - Use grep to find files by content - Use read before editing any file - Use edit for small changes, write for new files or full rewrites - Use bash to run tests, builds, and git commands Constraints: - Never modify test files to make tests pass - Never delete code you don't understand - Always read a file before editing it - If a command fails, read the error and adjust Be concise. Explain what you're doing before each tool call."""这个提示词不长,但把关键规则都覆盖了。实际项目中,系统提示往往要长得多,会针对具体场景加更多约束。但核心结构就是这样:角色、流程、工具规则、约束。
4.5 跑起来:一个真实任务的完整执行记录
我拿一个真实任务跑了一遍,任务描述是:“项目里有个utils.py,里面的parse_config函数有 bug,配置文件里的注释行会导致解析失败。找到并修复它。”
Agent 的执行过程大致如下:
第一轮,模型决定先找文件。调用glob("**/utils.py"),返回src/utils.py。
第二轮,模型调用grep("parse_config", "src/utils.py"),定位到函数在第 45 行。
第三轮,模型调用read("src/utils.py", 40, 80),读取函数实现。发现代码用line.split('=')解析每一行,没有跳过以#开头的注释行。
第四轮,模型调用edit,把循环体改成先判断if line.strip().startswith('#') or not line.strip(): continue。
第五轮,模型调用bash("python -m pytest tests/test_utils.py"),测试通过。
第六轮,模型输出总结:“已修复 parse_config 函数,添加了注释行和空行的跳过逻辑,测试通过。”
整个过程 6 轮,没有向量检索,没有框架,就是 grep、read、edit、bash 四个工具的组合。这就是 coding agent 的真实工作方式。
5. 常见问题与排查技巧实录
5.1 模型不调用工具,直接瞎编答案
这是最常见的问题。模型看到任务后,不调用工具去查证,而是凭训练数据里的印象直接给答案。比如你问“项目里用了什么测试框架”,它直接说“pytest”,但其实项目用的是 unittest。
解决办法是在系统提示里明确要求“必须先搜索再回答”,并且在工具定义里把搜索工具的描述写清楚。另一个技巧是在用户消息里加一句“请先查看项目文件再回答”。实测下来,加了这句之后,模型调用工具的概率大幅提升。
如果还是不行,可以在主循环里加一个检查:如果模型第一轮没有调用任何工具就给出答案,就自动追加一条消息“请使用工具验证你的答案”,强制它再跑一轮。
5.2 edit 替换失败,旧字符串找不到或不唯一
edit 工具依赖模型提供的“旧字符串”在文件中精确匹配。失败通常有两种:找不到,或者找到多个。
找不到的原因往往是模型记错了内容,或者文件在读取之后被其他操作修改了。解决办法是让模型在 edit 之前重新 read 一次文件,确保看到的是最新内容。有些 agent 会在 edit 失败时自动触发一次 read,然后把最新内容返回给模型让它重试。
不唯一的原因是模型给的旧字符串太短,在文件里出现了多次。解决办法是在提示词里要求模型“提供足够的上下文,确保旧字符串在文件中唯一”。通常多带几行上下文就能解决。
实操心得:edit 工具的错误信息要写得足够详细,比如“old_str 在文件中出现 3 次,分别在第 12、45、78 行”,这样模型能快速调整。错误信息越具体,模型自我修复的能力越强。
5.3 上下文爆炸,跑到一半就超限
长任务跑到一半上下文超限,是 coding agent 的经典问题。表现是模型开始胡言乱语,或者工具调用格式出错。
根本原因是消息历史太长。解决办法前面提过,核心是三点:工具结果截断、历史摘要、外部记忆。我实测下来,工具结果截断的性价比最高,实现简单效果明显。read 限制返回行数,grep 限制匹配数量,bash 限制输出长度,这三个截断一加,上下文压力立刻小很多。
如果任务确实很长,就需要历史摘要。我的做法是当消息数量超过阈值时,把最早的一批消息交给模型总结成一段话,然后用这段摘要替换掉原始消息。摘要里保留关键的文件路径、函数名、已做的修改,丢弃具体的工具输出。
5.4 模型陷入死循环,反复调同一个工具
有时候模型会卡在一个循环里,比如反复 grep 同一个关键词,或者反复 read 同一个文件。这通常是因为它没找到想要的信息,又不知道换什么策略。
防护措施有两个。一是设置最大轮数,超过就强制终止。二是检测重复调用,如果连续几轮调用了相同的工具和参数,就注入一条提示“你已经调用过这个工具了,请换一个策略”。实测这个提示很有效,模型看到后会主动换思路。
还有一个隐蔽的死循环是 edit 反复失败。模型改一次不成功,再改一次还是不成功,来回好几次。这种情况通常是旧字符串本身有问题,比如包含了不可见字符。解决办法是让模型 read 出精确内容后再 edit,或者干脆用 write 重写整个文件。
5.5 常见问题速查表
| 问题 | 可能原因 | 解决思路 |
|---|---|---|
| 模型不调工具直接回答 | 提示词约束不足 | 系统提示强调先搜索,加验证轮 |
| edit 找不到旧字符串 | 文件已变更或模型记错 | edit 前重新 read,失败时返回最新内容 |
| edit 匹配多个位置 | 旧字符串太短 | 要求提供更多上下文保证唯一 |
| 上下文超限 | 消息历史过长 | 截断工具结果,历史摘要,外部记忆 |
| 反复调同一工具 | 模型卡住 | 最大轮数限制,重复检测加提示 |
| bash 命令超时 | 命令卡死或耗时过长 | 设置超时,超时后返回错误让模型调整 |
| 修改了不该改的文件 | 约束不足 | 系统提示明确禁止修改测试文件等 |
6. 从这 11 个 agent 身上学到的设计原则
拆完这 11 个 agent,我最大的收获不是某个具体技巧,而是一套设计哲学。这套哲学可以总结成几条原则,不管你做什么类型的 agent,都值得参考。
第一条原则是用最简单的原语。文件系统、grep、shell,这些都是存在了几十年的东西,稳定、可靠、人人会用。与其发明新的抽象,不如把老工具用好。模型足够聪明,你给它简单的工具,它能组合出复杂的行为。
第二条原则是让模型做决策,让代码做执行。Agent 的智能来自模型,工程代码只负责把模型的决策落地。不要把业务逻辑硬编码在代码里,而是通过提示词引导模型。这样 agent 的行为可以随模型升级而提升,不需要改代码。
第三条原则是错误是信息,不是终点。工具失败、命令报错,这些都应该返回给模型,让它自己调整。一个健壮的 agent 不是不犯错,而是能从错误中恢复。
第四条原则是上下文是稀缺资源。每一 token 都要花在刀刃上。工具结果要截断,历史要摘要,无关信息要丢弃。上下文管理做得好不好,直接决定 agent 能不能完成长任务。
第五条原则是约束比能力更重要。给模型加能力容易,加约束难。但真正让 agent 可用的,往往是那些“不要做什么”的约束。不要改测试、不要删代码、不要瞎猜,这些约束让 agent 的输出可信。
这五条原则,比任何框架都值钱。框架会过时,原则不会。你把这五条吃透,不管以后模型怎么升级、工具怎么变化,你都能搭出好用的 agent。
最后分享一个我自己的小习惯:每次搭新 agent,我都会先写系统提示词,把工作流程和约束想清楚,再去写代码。提示词写明白了,代码往往很简单;提示词没想清楚,代码写再多也是白搭。这个顺序,希望对你有用。