图解 Agent 精华:Claude Code 源码 + RAG / Harness 工程

本文出处:小林 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。很多人停留在「它们都是调工具的」,一深挖就分不清谁是谁。

工具调用三层架构 + 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 用四层架构把它们组织起来:

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 的三个问题:

  1. Token 浪费:每轮都要输出一段 Thought 文本,一次任务循环 50 轮就是几万 Token 的浪费。
  2. 应用层代码复杂:要解析模型输出、区分 Thought/Action、提取调用,容易崩。
  3. 为「弱模型」设计: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 原语,作为天然的终止信号,语义清晰。
维度ReActTool-Use Loop
推理方式显式 Thought 文本模型内部 Extended Thinking
工具调用解析文本提取 ActionAPI 原生 tool_use
终止判断检测「Final Answer」API 原生 end_turn
Token 开销每轮要输出 Thought无额外开销
编排复杂度低(只需 if/else)

一句话:信任模型的推理能力,把应用层框架做得尽可能简单。

Plan Mode

复杂任务先规划再执行。它不是独立框架,而是通过 EnterPlanMode / ExitPlanMode 两个工具实现:

  1. 模型判断是复杂任务时自主进入(或用户 Shift+Tab 触发)。
  2. 进入后权限降为只读,只能用 Read/Grep/Glob 探索,把计划写入 .claude/plans/。每 5 轮对话系统会塞一张「小纸条」提醒模型「你还在 Plan Mode,别手痒改代码」。
  3. 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 行为反馈(最重要):不仅记规则,还要记 WhyHow to apply
  • project 项目动态:必须把相对日期转成绝对日期(「周四」→「2026-03-05」)
  • reference 外部指针:去哪找什么信息

不记什么——排除清单:核心原则是「可以从当前代码推导出来的信息,一律不存」。因为代码是活的,记忆是死的,存下来的 file:line 引用很快变成「权威的错误」,比没有记忆还糟。

怎么存:每条记忆是独立 .md 文件 + 一个 MEMORY.md 索引(硬上限 200 行 / 25KB,同时检查行数和字节数)。索引常驻 System Prompt,详情按需加载。

怎么召回——用小模型(Sonnet)当秘书

  1. 只读每个记忆文件前 30 行的 frontmatter(不读全文),扫描出清单。
  2. 把清单 + 用户输入发给 Sonnet,让它选出最多 5 条相关记忆(只返回文件名)。
  3. 加载选中记忆的完整内容,作为 <system-reminder> 注入。

还有陈旧度检测:超过 1 天的记忆自动附加警告「这条记忆已 N 天,引用前请先对照当前代码验证」。以及并行预取:用户提交消息后立刻启动 Sonnet 检索,与主模型 API 调用并行,几乎零额外延迟。

三句话概括:记该记的,不记能推导的;存索引,按需加载详情;用小模型做秘书,大模型做决策。

六、上下文窗口管理:五层压缩金字塔

这是整个 Claude Code 最复杂、面试最爱拷打的部分。

为什么 Agent 分分钟爆窗口

普通聊天每轮几十到几百 token,但 Agent 的窗口压力来自三处叠加:

  1. 开局就是大头:System Prompt + 工具描述 + CLAUDE.md,固定开支 5k~10k token。
  2. 工具调用双倍记账:每次调用产生 tool_use(我要调什么)+ tool_result(返回内容)两条消息,都进窗口。
  3. 大文件 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
2Snip 砍远古消息开头探索性消息无用
3Micro-Compact 时间衰减距上次 API 调用 > ~60 分钟
4Context Collapse 读时投影上下文达 90%(95% 升级)
5Auto-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):

  1. 通用黑名单:禁止「能派新 subagent / 能问用户 / 能切规划模式 / 能停其他任务」的工具(防递归嵌套、防抢对话权、防污染任务表)。
  2. 自定义 Agent 加严:用户自己写的 Agent 多套一层黑名单。
  3. 后台异步 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 像个带「信箱」的独立员工。

父子 Agent 消息驱动通信

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_000 ms),把同步工具调用降级成异步通知,让父 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 设计原则

  1. 上下文隔离要按字段粒度做:每个状态单独决策(克隆/关闭/保留/计数),而非粗暴给个空 context。
  2. 角色分离:协调和干活分开,不要一个 Agent 身兼二职。
  3. 并发优先:异步 + 消息队列是并发的基础。
  4. 合成不转发:协调者要理解中间结果,不当传话筒。
  5. 扁平不递归:用工具权限把层级限制在两层(协调者 + 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."

grep vs RAG 对比

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 全链路

RAG 分离线建库在线检索生成两段。

离线建库:文档解析清洗 → 切分 Chunking(固定大小 / 语义 / 结构化,块太小丢上下文、太大引噪声,经验值 500~2000)→ Embedding 向量化 → 建索引(向量库 + BM25 倒排)。

在线检索生成

  1. Query 预处理:意图识别、纠错、Query 改写。
  2. 多路召回——这是重点。没有任何一种检索方式全能,得组合互补盲区:
    • 向量检索:擅长语义、同义词(「退货」≈「申请售后」),但对精确词(型号、缩写、数字)差。
    • BM25 关键词:精确词匹配强(TF-IDF 改进版:词在本文档出现多、在全库出现少→权重高),但不理解语义。
    • 多 Query 扩展:用 LLM 把问题改写成 3~5 个角度,覆盖「用户问法 ≠ 文档表述」的差异(「多久送到」vs「配送时效」),召回覆盖率能升 10%~20%。
    • 三路结果用 RRF(倒数排名融合) 合并:只看排名不看原始分数,巧妙绕开「向量相似度和 BM25 分数不可比」的问题(score = Σ 1/(k+rank),k 常取 60)。
  3. Rerank 精排:用 Cross-Encoder(如 bge-reranker)做深度打分,把真正相关的筛到最前,顺便缓解 Lost in the Middle。
  4. 组装 Prompt + 生成:上下文 + 问题喂 LLM。
  5. 后处理 / 评估:引用溯源、事实校验。

怎么评估 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 直译是「马具 / 缰绳」。一个简洁的等式概括了它:

Prompt ⊂ Context ⊂ 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 的坑、字节级缓存对齐),才是从生产环境里真正活下来的工程范本。