图解 Agent 精华:Claude Code 源码 + RAG / Harness 工程
目录
总结小林 coding「图解 Agent」系列:Agent 基础概念(FC/MCP/Skill/A2A)、Claude Code 四层架构与 Tool-Use Loop、上下文窗口五层压缩、多 Agent 机制,以及 grep vs RAG、RAG 全链路、Harness Engineering
本文出处:小林 coding 公众号「图解 Agent」系列。本文挑选其中关于 Agent 基础、Claude Code 源码、RAG 与 Harness 的若干篇做归纳整理,并结合公开资料(Anthropic / OpenAI 工程博客、社区讨论)补充少量内容。所有 Prompt 原文均为英文,引用的中文为意译并保留关键术语;所有代码为示意性精简版。
Claude Code 客户端源码泄漏后(npm 打包时手抖把 .map 文件一起传了上去,51 万行核心代码全网裸奔),业界发现:人家 80% 的代码根本不是在搞「让 AI 更聪明」的黑科技,而是在死磕可靠性——也就是最近很火的所谓 Harness Engineering(线束工程):与其祈求大模型变聪明,不如老老实实给这匹野马套上缰绳,用系统去约束它的行为。
全文从「Agent 是什么」讲到「Claude Code 怎么实现」,再上升到「为什么这么设计」:
- 基础:Agent 基础概念(FC / MCP / Skill / A2A)
- 实现:Claude Code 的四层架构、Tool-Use Loop、System Prompt、记忆、上下文五层压缩、多 Agent
- 哲学:为什么用 grep 而非 RAG、RAG 全链路补课、Harness Engineering
零、先打地基:Agent 基础概念
在拆 Claude Code 之前,先把几个最容易被面试官追问、又最容易混为一谈的概念理清楚:Function Calling、MCP、Skill、A2A。很多人停留在「它们都是调工具的」,一深挖就分不清谁是谁。
一句话记住它们的层级关系:
- Function Calling = 语言。模型输出「我要调哪个工具、参数是什么」的结构化意图,是模型与工具沟通的「语法」。谁负责决策(模型)、谁负责执行(应用层),由它划清。
- MCP(模型上下文协议)= 工具箱。把外部工具 / 数据源用统一接口标准化暴露给模型。MCP 解决的是「Agent 连工具」。
- Skill = 操作手册。Anthropic 2025/10 推出、12 月开源为跨平台规范,把「完成某类任务的知识 + 流程」打包成可复用模块。和 MCP 互补:MCP 给能力,Skill 给「怎么用这些能力把活干成」。
- A2A(Agent 间协作协议)= Agent 连 Agent。Google 2025/04 推出(后捐给 Linux 基金会),解决的是 Agent 之间的协作,和「Agent 连工具」的 MCP 是不同维度。
那 Agent 和聊天机器人 / Workflow 到底差在哪?
- ChatBot / Copilot:一问一答、一次性预测,没有自主循环。
- Workflow:按预定义的流程图(DAG)一步步走,路径是写死的。
- Agent:核心是「感知 → 决策 → 行动」的自主循环——下一步做什么由模型自己定,不是写死的流程。最经典的范式是 ReAct(Reasoning + Acting,每轮 Thought → Action → Observation)。后面会看到 Claude Code 连 ReAct 都嫌重,改用了更简洁的 Tool-Use Loop。
理解了这层地基,下面正式进入 Claude Code。
一、Claude Code 是什么
Claude Code 是 Anthropic 官方的编程 Agent,能直接在终端里读代码、改文件、跑命令、管 Git。它的本质是一个 AI Agent,区别于一问一答的 ChatBot 和做补全的 Copilot:
- Agent 的核心是「感知-决策-行动」的自主循环。你给一个目标(「帮我修这个 bug」),它自己决定先读哪个文件、跑什么命令、改哪行代码,循环几十轮直到完成。
- 关键在于:大模型自己决定下一步做什么,不是按预定义流程图走。
二、四层分层架构
一个自主编程 Agent 要处理的事情非常多:调 API、执行 40+ 种工具、管理权限、压缩上下文、维护记忆、多 Agent 协作……Claude Code 用四层架构把它们组织起来:
| 层 | 职责 | 关键原则 |
|---|---|---|
| 引擎层 | Agent 的「大脑」,负责协调、分发、决策 | 不含任何业务逻辑,新增能力只需加一个工具 |
| 工具层 | 全部「能力」,40+ 工具 | 每个工具强制声明三个安全属性 |
| 服务层 | 共享基础设施 | 调大模型 API、上下文压缩、MCP 协议 |
| 安全与治理层 | 罩在所有层之上的安全网 | 权限系统、Hook 系统、Bash 安全分析 |
工具层的「三个安全属性」由类型系统强制要求,漏掉任何一个代码就编译不过:
- 这个工具是只读的还是会改东西?
- 是否具有破坏性、需要额外确认?
- 能不能和其他工具同时执行?
「每一把刀都有刀鞘,从出厂就配好了安全机制。」
三、Agent 工作模式:Tool-Use Loop 而非 ReAct
很多人以为 Claude Code 用的是经典的 ReAct(Reasoning + Acting,每轮输出 Thought → Action → Observation)。实际上它没有,而是用了更简洁的 Tool-Use Loop。
ReAct 的三个问题:
- Token 浪费:每轮都要输出一段 Thought 文本,一次任务循环 50 轮就是几万 Token 的浪费。
- 应用层代码复杂:要解析模型输出、区分 Thought/Action、提取调用,容易崩。
- 为「弱模型」设计:Opus 级别的强模型推理能力已经够强,不需要显式 Thought 引导。
Tool-Use Loop 的核心就是一个 while(true):
while (true) {
// 1. 压缩上下文(五步从轻到重)
// 2. 调用大模型 API,流式接收
for await (const event of streamAPI(params)) {
yield event
}
// 3. 分析模型返回
if (response.stopReason === 'end_turn') break // 完成,跳出
// 4. 执行工具调用(并发/串行编排)
const toolResults = await executeToolCalls(toolUseMessages)
// 5. 更新 state,继续循环
continue
}
它为什么比 ReAct 好:
- Extended Thinking 让推理在「模型内部」完成,不占上下文,不需解析。
- API 原生支持
tool_use,消除了文本解析。 end_turn是 API 原语,作为天然的终止信号,语义清晰。
| 维度 | ReAct | Tool-Use Loop |
|---|---|---|
| 推理方式 | 显式 Thought 文本 | 模型内部 Extended Thinking |
| 工具调用 | 解析文本提取 Action | API 原生 tool_use |
| 终止判断 | 检测「Final Answer」 | API 原生 end_turn |
| Token 开销 | 每轮要输出 Thought | 无额外开销 |
| 编排复杂度 | 高 | 低(只需 if/else) |
一句话:信任模型的推理能力,把应用层框架做得尽可能简单。
Plan Mode
复杂任务先规划再执行。它不是独立框架,而是通过 EnterPlanMode / ExitPlanMode 两个工具实现:
- 模型判断是复杂任务时自主进入(或用户
Shift+Tab触发)。 - 进入后权限降为只读,只能用 Read/Grep/Glob 探索,把计划写入
.claude/plans/。每 5 轮对话系统会塞一张「小纸条」提醒模型「你还在 Plan Mode,别手痒改代码」。 - 调
ExitPlanMode,用户审批后恢复读写权限,按计划实施。
设计精髓:工具即能力。对模型来说 Plan Mode 不是「模式切换」,只是调了两个工具,引擎层的
while(true)完全不用特殊处理。
四、System Prompt 的构造
System Prompt 是 Claude Code 的灵魂,但它不是静态文本,而是十几个 Section 动态拼接。几条最值得抄作业的设计:
- 角色定义:自称
interactive agent而非assistant,暗示主动行动。 - 安全约束「先肯定再约束」:先说「授权的安全测试、CTF 可以做」,再说「DoS、供应链攻击不能做」,比纯禁止清单效果好。
- 代码风格「少即是多」:不主动加功能、不顺手重构。「三行相似的代码比一个过早的抽象更好。」
- 失败处理「先诊断再换方案」:读报错、检查假设、针对性修复,既不盲目重试也不草率放弃。
- 操作安全用两个维度判断风险:可逆性(reversibility) 和 影响范围(blast radius)。本地可逆操作放行,
git push/发消息等必须确认。而且授权是一次性、有范围的——批准一次 push 不代表以后都自动 push。 - 优先专用工具而非 Bash:用 Read 而非
cat、用 Edit 而非sed。动机是可审查性和专用权限检查。 - Git 安全协议:绝不改 git config、绝不破坏性命令、绝不跳过 hooks。最妙的一条:始终创建新 commit 而不是
--amend——因为 pre-commit hook 失败时 commit 根本没发生,--amend会改到上一个不相关的 commit,导致代码丢失。 - 输出效率:工具调用之间不超过 25 个词,最终回复不超过 100 个词。
分割线与三级缓存
System Prompt 中插入了一个分割标记 __SYSTEM_PROMPT_DYNAMIC_BOUNDARY__:
- 分割线之上(角色、安全、行为准则……)对所有用户完全一样。
- 分割线之下(环境信息、CLAUDE.md、记忆、MCP)每个用户都不同。
为什么要分?因为 Claude API 有 Prompt Cache:前缀完全相同就能复用上次计算,费用降低 90%。分割线之上的内容可被全球用户共享缓存。由此形成三级缓存体系:全局缓存 → 组织缓存 → 会话缓存。
五、记忆系统
每次启动都是全新会话,但用户偏好、项目背景需要跨会话保持。Claude Code 没用向量数据库(因为要记的是「不要 mock 数据库」这种结构化行为指令,向量相似度检索效果差),而是设计了一套独特方案。
记什么——四类型封闭集合(不能随便加新类型,逼 Agent 做分类决策):
const MEMORY_TYPES = ['user', 'feedback', 'project', 'reference'] as const
user用户画像:角色、偏好、知识水平feedback行为反馈(最重要):不仅记规则,还要记 Why 和 How to applyproject项目动态:必须把相对日期转成绝对日期(「周四」→「2026-03-05」)reference外部指针:去哪找什么信息
不记什么——排除清单:核心原则是「可以从当前代码推导出来的信息,一律不存」。因为代码是活的,记忆是死的,存下来的 file:line 引用很快变成「权威的错误」,比没有记忆还糟。
怎么存:每条记忆是独立 .md 文件 + 一个 MEMORY.md 索引(硬上限 200 行 / 25KB,同时检查行数和字节数)。索引常驻 System Prompt,详情按需加载。
怎么召回——用小模型(Sonnet)当秘书:
- 只读每个记忆文件前 30 行的 frontmatter(不读全文),扫描出清单。
- 把清单 + 用户输入发给 Sonnet,让它选出最多 5 条相关记忆(只返回文件名)。
- 加载选中记忆的完整内容,作为
<system-reminder>注入。
还有陈旧度检测:超过 1 天的记忆自动附加警告「这条记忆已 N 天,引用前请先对照当前代码验证」。以及并行预取:用户提交消息后立刻启动 Sonnet 检索,与主模型 API 调用并行,几乎零额外延迟。
三句话概括:记该记的,不记能推导的;存索引,按需加载详情;用小模型做秘书,大模型做决策。
六、上下文窗口管理:五层压缩金字塔
这是整个 Claude Code 最复杂、面试最爱拷打的部分。
为什么 Agent 分分钟爆窗口
普通聊天每轮几十到几百 token,但 Agent 的窗口压力来自三处叠加:
- 开局就是大头:System Prompt + 工具描述 + CLAUDE.md,固定开支 5k~10k token。
- 工具调用双倍记账:每次调用产生
tool_use(我要调什么)+tool_result(返回内容)两条消息,都进窗口。 - 大文件 Read 杀伤力巨大:一个源文件几千上万 token,读三五个桌子就满了。
加大窗口也救不了,三个硬伤:钱(token 消耗几何级上涨)、慢(Attention 平方复杂度,TTFT 变高)、Lost in the Middle(长上下文中模型对中间段记忆模糊,这是注意力机制的固有特性,窗口扩到 1 亿 token 也一样)。
常见方案为什么不够看
- 滑动窗口:从最老的砍。但 Agent 最关键的全局指令往往在开头,砍掉就失控;工具调用有上下文依赖,砍掉
tool_result后面就无源之水。本质是「用遗忘换续航」。 - 每 N 轮摘要:触发时机死板、粒度太粗,机械主义地破坏对话连贯性。
- 向量召回:Agent 上下文强时序依赖,向量按相似度召回会乱序;
tool_use/tool_result成对出现会被切开;top-k 一定漏掉关键决策点。RAG 检索文档行,Agent 上下文不行。
Claude Code 的五层金字塔(从轻到重)
核心理念:压缩一定有信息损失,能不压就不压,必须压时从最轻手段开始。
| 层 | 手段 | 触发条件 | API 开销 |
|---|---|---|---|
| 1 | 大结果存磁盘 | 单工具结果 > ~50KB | 零 |
| 2 | Snip 砍远古消息 | 开头探索性消息无用 | 零 |
| 3 | Micro-Compact 时间衰减 | 距上次 API 调用 > ~60 分钟 | 零 |
| 4 | Context Collapse 读时投影 | 上下文达 90%(95% 升级) | — |
| 5 | Auto-Compact 全量摘要 | 上下文达 ~93%(上限 -13K) | 一次 API 调用 |
- 第 1 层:完整内容写磁盘,消息里只留 2KB 预览,需要时再 Read 拿回。
- 第 2 层:删掉开头无用消息,插入边界标记,纯本地操作。释放的 token 数会传给第 5 层避免重复压缩。
- 第 3 层:清空「可重新获取」的工具结果(Read/Bash/Grep/Glob/WebSearch/Edit/Write),只留最近 5 个;子 Agent 输出、Task 状态这类「不可重复」结果绝不裁剪。
- 第 4 层:最巧妙——不修改原始消息,只在调 API 那一刻动态计算一个压缩视图。「写时不动、读时投影」,本地完整保留。
- 第 5 层:真正的全量重写,代价最高但效果最强。
5 层里有 3 层零 API 开销,每一层都在替下一层减负,绝大部分场景根本走不到第 5 层。
第 5 层 Auto-Compact 深度拆解
触发阈值用「距离上限的固定缓冲」而非比例:
const AUTOCOMPACT_BUFFER_TOKENS = 13_000
function getAutoCompactThreshold(model: string): number {
return getEffectiveContextWindowSize(model) - AUTOCOMPACT_BUFFER_TOKENS
}
为什么是 13k?基于摘要任务的 p99.99 输出长度统计算出来的(实际数据约 17.3k,留了安全冗余)。绝对值阈值的好处是可预测:窗口未来扩到 1M,触发线永远是「上限 -13k」。
全量重写:所有历史消息(哪怕是最近的)全部送进摘要器重写一份。反直觉,但因为 Lost in the Middle,保留最近 20 轮模型也看不清,不如全压成结构化精华。压缩后变成四段式:
function buildPostCompactMessages(result) {
return [
result.boundaryMarker, // 边界标记(自动/手动、压缩前 token、最后消息 ID)
...result.summaryMessages, // 摘要消息(前 N 轮全压这里)
...result.attachments, // 附件:最近文件、计划、技能、异步任务状态
...result.hookResults, // hook 执行结果
]
}
核心思想是信息分通道管理 / 信息半衰期:
- 语义信息(需求、技术方案)→ 走摘要,压成几句话不损失。
- 状态信息(文件读到第几行、子任务输出在哪)→ 走附件原样恢复,差一个字 Agent 就接不上。
- 永久信息(CLAUDE.md)→ 不进摘要,靠清空
getUserContext缓存在下一轮自动重新加载。 - 操作配置(System Prompt)→ 不参与压缩,用
buildEffectiveSystemPrompt重建,顺便刷新工具/权限/MCP 列表。
文件恢复策略(量化「最近文件」):
const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000
const POST_COMPACT_TOKEN_BUDGET = 50_000
const POST_COMPACT_MAX_FILES_TO_RESTORE = 5
最多恢复 5 个文件、每个 ≤5k token、总预算 ≤50k,按最近 Read 活跃度排序。
摘要 Prompt 设计(长达 200+ 行):
- 防呆:开头结尾各喊一遍「只能返回纯文本,禁止调用任何工具」(早期 Sonnet 4.6 经常无视一次警告,工程师干脆前后包夹)。
- 输出 XML 结构:
<analysis>(草稿区,最终剥离)+<summary>(9 部分固定清单)。 - 9 部分清单里灵魂是第 6 项 All user messages(枚举所有非 tool-result 用户消息,一句不能落,避免方向跑偏)和第 8 项 Current Work(用最细颗粒度描述当前进度,精确到文件和函数名)。
- 摘要用同一个主模型(不省钱换小模型):保证质量 + 复用 Prompt Cache。
接续机制:摘要开头包一句「本会话是从之前一次因上下文耗尽而中断的对话延续过来的」,让模型知道是接力而非从头。自动触发时打开 suppressFollowUpQuestions 开关,避免摘要里塞新问题打断任务。旧消息真的丢了(除非开 Kairos transcript 备份),一刀切反而干净。
两个带血的安全设计:
- 熔断(circuit breaker):auto-compact 连续失败 3 次就停止重试——曾有 1000+ 会话反复失败重试把 API 账单当烟花放。
- 递归守卫:压缩任务本身也调模型,会被标记
compact/session_memory来源,直接return false不再触发压缩,三行代码堵死无限递归。
上下文管理不是「省 token」的小聪明,而是「信息分通道管理」的工程哲学。Lost in the Middle 是注意力机制的固有特性,所以无论窗口怎么膨胀,主动管理信息结构永远不过时。
七、多 Agent 实现机制
为什么一个 Agent 不够用?让单 Agent 「调研 + 实现 + 评审」会有三个麻烦:上下文爆炸、职责混乱、没法并发。Multi-Agent 就像老板带团队:把任务拆给职责清晰的专家,老板只管派活、收结果、做决策。
Claude Code 里有三套机制:常规 Subagent(父子型)、Fork Subagent(父子型优化版)、Coordinator 模式(主从型)。
Subagent 的隔离机制
多 Agent 共处一个进程,隔离做不好就乱套。Claude Code 在两个维度做隔离:
① 工具隔离——三道准入门(filterToolsForAgent):
- 通用黑名单:禁止「能派新 subagent / 能问用户 / 能切规划模式 / 能停其他任务」的工具(防递归嵌套、防抢对话权、防污染任务表)。
- 自定义 Agent 加严:用户自己写的 Agent 多套一层黑名单。
- 后台异步 Agent 走白名单:默认不准用,只放行明确列出的少数工具,比黑名单更保险。
② 上下文隔离——按字段粒度决策(createSubagentContext)。不是「全共享」也不是「全新建」,而是每项状态单独判断:
function createSubagentContext(parentContext, overrides) {
return {
readFileState: cloneFileStateCache(parentContext.readFileState), // ① 克隆,避免污染父视图
setAppState: () => {}, // ② 写全局状态关闭,避免抢
setAppStateForTasks: parentContext.setAppStateForTasks ?? parentContext.setAppState, // ③ 任务注册通路保留,避免孤儿进程
agentId: overrides?.agentId ?? createAgentId(), // ④ 独立 ID
queryTracking: { chainId: randomUUID(), depth: (parentContext.queryTracking?.depth ?? -1) + 1 }, // 深度 +1,防失控嵌套
}
}父子 Agent 通信:消息驱动而非函数调用
如果用「调函数等返回」,父 Agent 会被同步阻塞死(5 分钟任务期间啥也干不了),也无法并发。所以 Claude Code 用消息队列 + 异步通知——每个 subagent 像个带「信箱」的独立员工。
subagent 的「员工档案」里关键是 pendingMessages 数组(信箱):
type LocalAgentTaskState = {
agentId: string
status: TaskStatus // pending/running/completed/failed/killed
result?: AgentToolResult
pendingMessages: string[] // 信箱:父 agent 扔进来的待处理消息
// ...
}
- 父 → 子:父调
SendMessage往信箱末尾追加消息后立刻返回;子在每轮循环边界自己捡字条,作为「用户消息」注入对话。若子已停止,则从磁盘 transcript 自动唤醒恢复。 - 子 → 父:把完成通知拼成一段
<task-notification>XML,伪装成用户消息注入父对话。用 XML 是因为 LLM 对 XML 友好、纯文本可直接塞进历史、且天然复用 agentic loop 处理逻辑,父 Agent 不需要额外状态机去「等通知」。 - auto-background:subagent 超过 2 分钟未完成自动转后台(
120_000ms),把同步工具调用降级成异步通知,让父 Agent 先干别的。
通信体系就两个关键字:异步 + 消息。没有锁、没有回调地狱,天然支持多 subagent 并发。
Fork Subagent:省钱又省延迟的隐藏大招
Claude Code 的 System Prompt 上万 token,每派一个有独立 prompt 的 subagent,API 就要从头算一遍这一万多 token——成本黑洞。而 Prompt Cache 命中条件是字节级完全相同(一个空格不一样都不命中)。
Fork Subagent 的思路:派一个 API 请求前缀跟父 Agent 字节级相同的「分身」,复用父的缓存(成本降到约 10%)。要对齐五样东西(CacheSafeParams):System Prompt、用户上下文、系统上下文、工具池顺序与定义、对话历史前缀。
一个工业级细节——Fork Agent 的 getSystemPrompt: () => '' 直接返回空串,因为它不重新生成 prompt(重新生成可能差一个字符导致缓存失效),而是直接用父 Agent 已渲染好的字节。
适用场景:需要继承父 Agent 完整上下文、「派个分身试试另一条路」(如生成 PR 描述、post-turn 总结)。与 Coordinator 模式互斥。
启示:成本优化本身就是能力的一部分。成本降到 10%,原本不敢派的活现在都能派了,Agent 系统的能力边界随之扩大。
Coordinator 模式:真正的并行协作
需要 CLAUDE_CODE_COORDINATOR_MODE=1 + 编译开关显式开启。开启后主 Agent 退化成纯协调者——不干活,只做三件事:派 worker、收结果、合成答案。
主 Agent 多了一套内部工具(worker 用不了,形成递归防护):TEAM_CREATE / TEAM_DELETE / SEND_MESSAGE / SYNTHETIC_OUTPUT / 停止 worker。
「并行是超能力」:派 worker 的工具调用可在同一条 assistant 消息里出现多次,底层一起并发执行。串行三个模块要等十分钟,并行只要三分多钟。
典型任务流水线四阶段:
| 阶段 | 谁来做 | 目的 |
|---|---|---|
| 调研 | Workers(并行) | 调查代码库、找文件、理解问题 |
| 合成 | 协调者本人 | 读懂发现、写实现规格 |
| 实现 | Workers | 按规格修改、提交 |
| 验证 | Workers(新 worker) | 测试改动是否真的工作 |
关键哲学:协调者必须「理解」而不能「转发」——不能偷懒让 worker「based on your findings, implement」,要自己读懂 findings 写成规格再派下去,否则协调者就没有存在价值。
Continue vs Spawn:新任务跟 worker 上下文相关就续命老 worker(沟通成本低),无关或走偏就派新 worker;验证这种需要「新鲜眼光」的工作永远派新 worker(不能让写代码的 worker 自己验自己)。
5 条 Multi-Agent 设计原则
- 上下文隔离要按字段粒度做:每个状态单独决策(克隆/关闭/保留/计数),而非粗暴给个空 context。
- 角色分离:协调和干活分开,不要一个 Agent 身兼二职。
- 并发优先:异步 + 消息队列是并发的基础。
- 合成不转发:协调者要理解中间结果,不当传话筒。
- 扁平不递归:用工具权限把层级限制在两层(协调者 + worker),避免失控嵌套。
八、为什么 Claude Code 用 grep 而不用 RAG
这是字节面试的高频题。Claude Code 这个「最好用的 AI 编程工具之一」,反其道而行——连 embedding 和向量数据库的影子都没有,就靠 Glob + Grep + Read 三件套加读文件这种最朴素的方式获取代码上下文。源码泄漏后社区确认:代码库里完全不存在 RAG 管线、没有向量库、没有 embedding 计算。
不是 Anthropic 没钱搞向量库。创建者 Boris Cherny 公开承认:早期版本真的用过 RAG(Voyage 的 embedding + 本地向量索引,效果「还行」),但很快发现 Agentic Search 全面碾压 RAG,原话是 "Plain glob and grep, driven by the model, beat everything."
RAG 在代码场景的几个坑
RAG 在「静态文档、自然语言、模糊匹配」是利器,但代码恰好相反——动态、结构化、要精确:
- 切片破坏结构:代码按 chunk 切,很容易把一个函数 / 调用链切断,检索到 controller 那层却追不到底层实现。
- 向量近似不准:要找
getUserById却把一堆相似函数糊在一起;而代码搜索里「精确」本身就是一切。 - 索引滞后:代码一天 push 好几次,每改一个文件 embedding 就过时,要实时同步成本比 RAG 本身还高。
- 冷启动:RAG 要先建索引(分钟级),违背 Claude Code「打开就能用」的理念。
- 安全:代码极敏感,不愿发给第三方做 embedding;本地部署又要算力。
Claude Code 的反向思路:把检索决策权还给模型
它的范式像「现场探案」:模型说「我先 Grep 一下」→ 看结果 →「UserService 比较像,Read 一下」→ 看内容 →「找到了」。这是一个 LLM 驱动的多轮迭代循环,Anthropic 把它叫 Agentic Search。
几个工程细节值得抄:
- Grep 单独包成工具(不让模型直接跑 bash grep):一是单独画一道权限闸门更安全,二是能提供结构化输出(行号、上下文行、按文件分组,还支持「只返回文件名 / 只返回数量」三种粒度,省 token)。
- 底层用 ripgrep(Rust 写的):多线程并行、自动尊重
.gitignore(不搜node_modules),性能甩老牌 grep 十倍(Linux 内核基准 0.06s vs 0.67s)。还封了一层保护:默认最多返回 250 行,防止撑爆上下文。 - Read 永远现 stat 磁盘、不缓存不索引,保证拿到的是最新内容。
- 派 Explore 子 Agent 去探索:grep 几十次的中间结果都留在子 Agent 自己的上下文里,不污染主 Agent——这正是前面多 Agent 那节讲的隔离机制的用武之地。
grep 本身不强,但「让 LLM 自己决定每一轮 grep 什么」就强了。
六个原因 + 一个分水岭
Claude Code 不用 RAG 的六个原因:冷启动(毫秒 vs 分钟)、实时性(现读 vs 滞后)、精确性(确定 vs 近似)、Token 经济(按需 vs 全量)、可解释性(透明 vs 黑盒)、决策权(还给模型 vs 替模型筛)。
更深的分歧在哲学层:
- RAG 派的潜台词:LLM 不够强,所以工程要替它把材料准备好(chunking / embedding / 召回,本质是「替模型做决定」)。
- grep 派的潜台词:LLM 已经足够强,工程的角色是给它准备好工具、把决策权还给它。
这也呼应了一个时代背景:2023 年瓶颈是「检索」(模型读取器小、慢、贵),2026 年瓶颈变成「对混乱上下文的推理」(读取器大、快、便宜了),策略随之变成「检索器笨但高召回,模型自己做重活」。当然 RAG 没死——巨型跨仓库代码检索、纯语义模糊查询仍是它的主场;Cursor 也走 grep + 向量的混合路线。
九、补课:RAG 全链路
上一节是「为什么代码不用 RAG」,但 RAG 在文档问答、知识库这类场景仍是主力。这里把 RAG 全链路补一补,方便对照理解「检索」这件事到底有多少工程。
RAG 分离线建库和在线检索生成两段。
离线建库:文档解析清洗 → 切分 Chunking(固定大小 / 语义 / 结构化,块太小丢上下文、太大引噪声,经验值 500~2000)→ Embedding 向量化 → 建索引(向量库 + BM25 倒排)。
在线检索生成:
- Query 预处理:意图识别、纠错、Query 改写。
- 多路召回——这是重点。没有任何一种检索方式全能,得组合互补盲区:
- 向量检索:擅长语义、同义词(「退货」≈「申请售后」),但对精确词(型号、缩写、数字)差。
- BM25 关键词:精确词匹配强(TF-IDF 改进版:词在本文档出现多、在全库出现少→权重高),但不理解语义。
- 多 Query 扩展:用 LLM 把问题改写成 3~5 个角度,覆盖「用户问法 ≠ 文档表述」的差异(「多久送到」vs「配送时效」),召回覆盖率能升 10%~20%。
- 三路结果用 RRF(倒数排名融合) 合并:只看排名不看原始分数,巧妙绕开「向量相似度和 BM25 分数不可比」的问题(
score = Σ 1/(k+rank),k 常取 60)。
- Rerank 精排:用 Cross-Encoder(如
bge-reranker)做深度打分,把真正相关的筛到最前,顺便缓解 Lost in the Middle。 - 组装 Prompt + 生成:上下文 + 问题喂 LLM。
- 后处理 / 评估:引用溯源、事实校验。
怎么评估 RAG? 业界常用 Ragas(LLM-as-a-Judge),拆成检索和生成两个维度:
| 维度 | 指标 | 衡量什么 |
|---|---|---|
| 检索 | Context Precision | 相关 chunk 是否排在前面 |
| 检索 | Context Recall | 相关内容有没有被漏掉 |
| 生成 | Faithfulness(忠实度) | 回答是否都能被上下文支撑(抗幻觉核心) |
| 生成 | Answer Relevancy | 回答跟问题意图的匹配度 |
幻觉抑制的关键就是 Faithfulness——让回答的每个陈述点都能溯源到检索上下文,配合分数阈值过滤掉不相关 chunk,避免噪声把模型带偏。
十、Harness Engineering:给大模型套缰绳
前面所有设计——五层压缩、记忆系统、多 Agent 隔离、grep 探案——其实都属于同一个更大的命题:Harness Engineering。这是 2026 年 Q1 应用层最具统治力的热词。
Harness 直译是「马具 / 缰绳」。一个简洁的等式概括了它:
Agent = Model + Harness
翻译成人话:一个 Agent 系统里,除了模型本身,几乎所有决定它能不能稳定交付的东西都属于 Harness——工具、上下文文件、记忆系统、评估机制、约束规则、恢复策略。反过来:Harness = Agent − Model。
三次重心转移:Prompt → Context → Harness
AI 工程这几年重心换了三次,三者是包含关系而非替代关系:
- Prompt Engineering——让模型「听懂」:把话说明白。解决「模型不是不会,是你没说清」。
- Context Engineering——让模型「知道」:召回 + 压缩 + 组装,在合适时机把正确信息送进上下文。Agent 火了之后才凸显。
- Harness Engineering——让模型「做对」:前两代关注「怎么让模型更会想」,Harness 关注「怎么让模型不跑偏、跑得稳、出了错还能爬起来」。
边界一层比一层大:Prompt ⊂ Context ⊂ Harness。分水岭在于——单轮对话 Prompt 就够;需要外部知识 Context 关键;一旦进入「长链路、可执行、低容错」的真实场景,Harness 几乎不可避免。
把前面的设计串起来看
回头看,Claude Code 源码里那些「带血」的细节,全是 Harness 的具体落地:
- 约束行为:System Prompt 的 Git 安全协议、操作可逆性判断、
--amend防坑。 - 管住失忆:六层记忆体系 +
autoDream后台「记忆大扫除」+ Context Reset(压不动就直接清空换新会话,比单纯压缩更激进)。 - 管住虚标完成:用 JSON 物理锁 + Generator-Evaluator 对抗(验证员被要求「try to break it」,只输出 PASS / FAIL / PARTIAL)。
- 管住脑容量:五层压缩金字塔 + 连续失败 3 次熔断。
- 可插拔:Hooks 在流水线 8 个节点埋插槽、44 个 feature flag——把「壳」变成开放平台。
一句话收口
换模型提升的是天花板,搭 Harness 提升的是落地能力。在模型迭代放缓的今天,Harness 的提升空间可能比想象的大得多——「模型是地板,harness 才是天花板」。
如果你最近在做 Agent,别再把精力全花在调模型、调提示词上。回头看看你的 Harness 长啥样:有没有规则文件、校验闭环、任务编排、评估机制、失败恢复、学习闭环。这些每一项,都能让你的 Agent 上一个台阶。
总结
把整条线串起来,从「Agent 是什么」到「Claude Code 怎么实现」再到「为什么这么设计」,最大的启示是同一句话:Agent 工程的胜负手不在「拼模型智商」,而在「拼系统求稳」。
- 基础:FC 是语言、MCP 是工具箱、Skill 是操作手册、A2A 连 Agent;Agent 的灵魂是自主循环。
- 架构:四层分层 + 工具即能力 + Tool-Use Loop,信任强模型、让框架尽可能简单。
- 上下文:五层压缩金字塔,能不压就不压;Auto-Compact 用「信息分通道管理」保住信息结构而非单纯省 token。
- 多 Agent:按字段粒度隔离 + 消息驱动通信 + Fork 复用缓存 + Coordinator 并行协作。
- 检索:代码场景把决策权还给 LLM(grep 现场探案),文档场景才上 RAG 全链路。
- 哲学:Prompt ⊂ Context ⊂ Harness。换模型提升天花板,搭 Harness 提升落地能力。
这 51 万行代码里大量「带着血味儿」的细节(13k 缓冲的 p99.99、连续失败 3 次熔断、--amend 的坑、字节级缓存对齐),才是从生产环境里真正活下来的工程范本。