你有没有注意到一件事:同一个 Claude 3.7 模型,在普通聊天窗口里写代码,跟在 Claude Code 里写代码,体验差距大得像两个产品。
不是模型不一样------模型权重完全相同。差的是包裹它的那一层工程架构,业界叫它 Agent Harness。
这层 harness 到底做了什么?Sebastian Raschka(《Build a Large Language Model From Scratch》作者)上个月发了一篇让我反复看了三遍的长文,把 coding agent 的六个核心组件拆得很透。今天我结合源码,把这六个零件讲清楚。
讲完你会明白一件事:2026 年 AI 编程的真正竞争,不在模型,在 harness 的工程质量。
先搞清楚:LLM / 推理模型 / Agent Harness 是三层不同的东西
很多人混用"大模型""推理模型""AI Agent"这几个词,但它们其实是三个不同的抽象层:
• LLM(基础语言模型):接收 token 序列,输出 token 序列。Claude 3.7 Sonnet、GPT-4o、Gemini 2.0 这些都是这层。它们本质上是一个复杂的条件概率函数。
• 推理模型(Reasoning Model):在 LLM 上叠加了 chain-of-thought 的系统性使用,比如 o3、Claude 3.7 Extended Thinking。不是新模型,是用法的提升。
• Agent Harness:包裹模型的工程框架。负责循环调用模型、管理工具、注入上下文、持久化状态。Claude Code、Codex CLI、Cursor 都是这层。
三层的关系大概是这样的:
| 层级 | 代表产品 | 核心工作 | 改变难度 |
|---|---|---|---|
| LLM | Claude / GPT-4o | token 预测 | 极高(需要训练) |
| 推理模型 | o3 / Extended Thinking | 系统性 CoT | 高(需要强化学习) |
| Agent Harness | Claude Code / Cursor | 工程编排 | 中(纯工程问题) |
这说明什么?Harness 层的改进是纯工程问题,不需要训练,但它的好坏直接决定用户体验。这也是为什么同一个模型,不同 harness 包裹出来的效果天差地别。
第一个零件:Agent Loop(观察-检查-选择-执行循环)
Harness 的核心是一个无限循环,它把 LLM 从"一问一答"变成"持续执行直到任务完成"的自主系统。伪代码大概是这样:
python
async def agent_loop(task: str, tools: list[Tool]) -> str:
messages = [{"role": "user", "content": task}]
while True:
# 1. OBSERVE: 调用模型
response = await llm.complete(
messages=messages,
tools=tools,
system_prompt=SYSTEM_PROMPT
)
# 2. INSPECT: 检查模型意图
if response.stop_reason == "end_turn":
return response.content # 任务完成
if response.stop_reason == "tool_use":
tool_calls = extract_tool_calls(response)
# 3. CHOOSE & ACT: 执行工具
tool_results = []
for call in tool_calls:
result = await execute_tool(call.name, call.input)
tool_results.append({
"tool_use_id": call.id,
"content": result
})
# 4. 将结果追加到对话历史
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
# 安全退出条件
if len(messages) > MAX_TURNS:
return "达到最大轮次限制"
这个循环看起来简单,但细节里藏着很多工程决策:
• 循环退出的条件是什么?仅仅依赖 stop_reason == "end_turn" 不够,还需要检测模型是否陷入工具调用死循环。
• 工具执行是串行还是并行?Claude Code 对独立的工具调用做并发处理,显著降低延迟。
• 出错了怎么办?工具执行失败需要把错误信息反馈给模型,让它自己决策重试还是换策略------这是 self-correction 能力的来源。
第二个零件:Repo Context 注入(代码库感知)
这是 coding agent 和通用 chatbot 最大的差异点。一个优秀的 coding agent 需要知道:当前仓库的目录结构、目标文件的内容、相关函数的定义、测试用例的期望行为。
但你不能把整个代码库塞进 context------哪怕 Claude 支持 200K token,大型仓库分分钟超限,而且大量无关代码会让模型分心。解决方案是"按需注入":
python
class RepoContextManager:
def __init__(self, repo_root: str):
self.repo_root = repo_root
self._file_tree = None
self._symbol_index = {} # 函数名 -> 文件路径:行号
def get_initial_context(self) -> str:
"""启动时注入:轻量级目录树 + 关键配置文件"""
tree = self._build_compact_tree(max_depth=3)
readme = self._read_if_exists("README.md", max_chars=2000)
return f"\n{tree}\n\n\n{readme}"
def search_symbol(self, name: str) -> list[dict]:
"""按需工具:搜索函数/类定义"""
results = []
for pattern in [f"def {name}", f"class {name}", f"function {name}"]:
matches = self._grep(pattern)
results.extend(matches)
return results
def read_file_chunk(self, path: str, start: int, end: int) -> str:
"""按需工具:读取文件片段,而非整个文件"""
lines = self._read_lines(path)
return "\n".join(lines[start-1:end])
def _build_compact_tree(self, max_depth: int) -> str:
"""生成紧凑目录树,自动忽略 node_modules/.git/build 等"""
ignore_patterns = {'.git', 'node_modules', '__pycache__',
'build', 'dist', '.gradle', 'Pods'}
# ... 实现省略
pass
注意这里有个关键设计:初始 context 只放"导航地图"(轻量目录树 + README),真正的代码内容通过工具调用按需获取。这和 RAG 的思路类似------先检索再精读,而不是一次性全量加载。
Claude Code 还做了一个更聪明的事:它会分析 git history,优先展示最近修改过的文件,因为这些文件很可能和当前任务相关。
第三个零件:Tool Use 设计(工具集的精心选型)
工具太少,agent 无法完成任务。工具太多,模型会选错工具("tool confusion"),而且每个工具定义都占 token。Claude Code 的工具集大概是这样分层的:
• 文件操作层:read_file / write_file / edit_file(注意:edit 比 write 重要,因为大文件只需要发送 diff,不用发送全文)
• Shell 执行层:bash(带超时、带输出截断、带沙盒限制)
• 代码智能层:search_files / grep / find_symbol(语义搜索而非纯字符串匹配)
• 验证层:run_tests / check_syntax / lint(执行完改动要能自我验证)
其中 edit_file 的设计最值得关注。一个朴素实现是每次都用 write_file 覆盖整个文件,但这有三个问题:上下文 token 消耗大、覆盖时容易引入不相关修改、无法追踪增量变更。更好的设计是基于 unified diff 格式:
python
# 工具调用示例:edit_file
{
"tool": "edit_file",
"input": {
"path": "src/auth/login.py",
"old_string": "def validate_token(token: str) -> bool:\n return token in VALID_TOKENS",
"new_string": "def validate_token(token: str) -> bool:\n if not token:\n return False\n return token in VALID_TOKENS and not is_revoked(token)"
}
}
# 对应的工具实现(简化版)
def edit_file(path: str, old_string: str, new_string: str) -> str:
content = Path(path).read_text()
if old_string not in content:
return f"ERROR: 找不到目标字符串,请重新确认上下文"
# 只替换第一次出现(避免批量误改)
new_content = content.replace(old_string, new_string, 1)
Path(path).write_text(new_content)
# 返回 diff 供模型确认
diff = unified_diff(
content.splitlines(keepends=True),
new_content.splitlines(keepends=True),
fromfile=f"a/{path}", tofile=f"b/{path}"
)
return "".join(diff)
让模型用 old_string/new_string 的方式描述修改,是个很聪明的设计:它强迫模型先"看清楚"要改的地方,减少幻觉式覆盖。如果 old_string 找不到,说明模型对文件内容的理解有误,工具直接报错,模型可以重新 read_file 校准。
第四个零件:Prompt Cache 稳定性(省钱的艺术)
这个零件很容易被忽略,但它可能是 Claude Code 在成本控制上最关键的工程优化。
Claude API 提供 prompt caching:如果请求前缀和上一次完全一样,可以复用 KV cache,输入 token 费用降低 90%。问题是,哪些内容构成"稳定前缀"?
• System prompt:完全固定,天然适合缓存。Claude Code 的 system prompt 据估计有数千 token,每次请求都缓存这部分。
• 工具定义(Tool Schemas):JSON 格式的工具描述,内容不变,适合缓存。
• 已完成的对话历史:上一轮的 assistant + user 消息,可以作为缓存前缀的一部分。
要实现稳定缓存,关键是保证前缀内容的顺序和内容不随意变动。一个常见的坑是:如果你的工具列表顺序在不同请求间会变化(比如动态加载工具),缓存就会频繁失效。
ini
# 显式标记 cache_control 的请求结构(Anthropic API)
messages = [
# cache point 1: system + tools(最稳定的部分)
{
"role": "user",
"content": [
{
"type": "text",
"text": SYSTEM_CONTEXT, # 上万 token 的系统上下文
"cache_control": {"type": "ephemeral"} # 标记缓存点
}
]
},
# ... 对话历史 ...
# cache point 2: 截至上一轮的完整对话
{
"role": "user",
"content": [
{
"type": "text",
"text": "继续执行",
"cache_control": {"type": "ephemeral"} # 第二个缓存点
}
]
},
# 当前轮次的新消息(不缓存)
{"role": "user", "content": current_message}
]
实际上 Claude Code 在每次对话结束后,累计 token 的 80-90% 理论上都可以走缓存。对于长会话,这直接决定了产品的经济可行性。
第五个零件:Memory 管理(记住什么,忘掉什么)
Coding agent 的 memory 需求和通用 chatbot 不同。它需要跨轮次记住:当前任务的整体目标、已经做了哪些修改、哪些路径已经探索过但失败了、用户明确表达的约束条件("不要动 tests/ 目录")。
这些东西如果都堆在 message history 里,会有两个问题:token 消耗随对话轮次线性增长;早期的关键信息会因为 context 过长而被"稀释"(模型注意力下降)。
更好的设计是分层 memory:
• Working Memory(短期,放在 context 里):当前任务状态、最近几轮工具调用结果
• Episodic Memory(中期,动态写入):重要的发现和决策,以结构化形式追加到 system prompt 的专用区域
• External Memory(长期,本地文件):CLAUDE.md / .claude/memory.json,持久化跨会话的仓库知识
python
# Claude Code 会在项目根目录查找 CLAUDE.md
# 这个文件相当于"仓库级 system prompt"
# 示例 CLAUDE.md 内容:
"""
## 项目约定
- 使用 pytest,不用 unittest
- 所有公开 API 必须有类型注解
- 数据库操作通过 repository pattern,不要直接操作 ORM
## 已知陷阱
- config.py 里的 DATABASE_URL 在测试环境和生产环境格式不同
- migrations/ 目录下的文件名必须按时间戳排序
## 常用命令
- 跑测试:make test
- 格式化:make fmt
- 部署:./scripts/deploy.sh staging
"""
这个设计极其实用:不需要每次都跟 agent 解释项目规范,也不会因为 agent 换了 session 就"失忆"。对于大型项目,一个维护良好的 CLAUDE.md 能把 agent 的有效性提升一个数量级。
第六个零件:长会话 Continuity(不崩溃的工程)
这是最容易被低估的一个零件。当你让 agent 做一个复杂任务("帮我重构整个认证模块"),它可能需要几十轮甚至上百轮工具调用。在这个过程中,有几个工程问题必须解决:
• Context 溢出:Claude 的 context window 是有限的,对话历史不能无限增长。解决方案是"滚动压缩"------周期性地让模型对之前的对话做摘要,用摘要替换原始内容。
• 任务追踪:在长任务中,模型可能忘记最初的目标,陷入局部循环。需要在每次循环开始时显式注入"当前任务目标"。
• 断点恢复:网络中断、用户手动打断,再恢复时不应该从头开始。需要持久化检查点。
python
class LongSessionManager:
COMPRESS_THRESHOLD = 80000 # token 数超过这个值时触发压缩
async def maybe_compress(self, messages: list, current_tokens: int):
if current_tokens