Claude Code 是一个面向软件工程任务的 AI Coding Agent。它通过 CLI(命令行界面)、IDE 插件等形态,让大模型直接参与代码阅读、修改、测试、提交说明生成和项目分析。
一个好用的 Coding Agent 不能只依赖 LLM(Large Language Model,大语言模型)本身。模型能力决定了推理上限,但真正落到工程任务时,还需要解决几个更具体的问题:
- 模型该以什么身份工作?
- 当前任务里哪些信息应该放进上下文?
- 工具调用要不要经过权限控制?
- 历史对话太长时怎么压缩?
- 子 Agent 怎么分工才不会污染主上下文?
- 失败、超时、用户取消、高危命令这些情况怎么处理?
这些问题分别对应三层工程:
| 层次 | 核心问题 | 在 Agent 系统中的作用 |
|---|---|---|
| Prompt Engineering(提示词工程) | 如何向模型描述角色、规则、工具和任务边界 | 奠定模型行为基线 |
| Context Engineering(上下文工程) | 让模型看到什么、不看到什么、什么时候压缩 | 保持长程任务的信息连续性 |
| Harness Engineering(脚手架/驾驭工程) | 在模型外部如何约束、调度、验证和恢复 | 让 Agent 可控、可观测、可干预 |
这三层不是互相替代的关系。Prompt 告诉模型应该怎么做,Context 让模型拥有完成任务所需的信息,Harness 则在模型外部提供安全边界和运行控制。缺少任何一层,Agent 都很容易在复杂任务中失控、失忆或过度执行。
一、Prompt Engineering:重点不只是“写提示词”,而是“组装提示词”
在成熟的 Agent 系统里,System Prompt 很少是一整段固定文本。它通常由多个模块动态拼接而成:有些内容对所有用户都相同,可以缓存;有些内容取决于当前项目、用户偏好、工具配置、MCP 服务器和会话状态,必须按需生成。
Claude Code 的 System Prompt 可以理解成一条动态流水线。
flowchart TD
A[用户输入] --> B[QueryEngine.ask]
B --> C[fetchSystemPromptParts]
C --> D1[defaultSystemPrompt]
C --> D2[systemContext]
C --> D3[userContext]
D1 --> E[buildEffectiveSystemPrompt]
E --> F[按优先级选择最终 System Prompt]
D2 --> G[appendSystemContext]
D3 --> H[prependUserContext]
F --> I[拆分缓存块]
G --> I
H --> J[插入用户消息最前面]
I --> K[调用 Claude API]
J --> K
它的关键不是“把提示词写得漂亮”,而是把提示词拆成可维护、可替换、可缓存、可注入的工程模块。
二、System Prompt 的组装流程
Claude Code 发起一次模型请求时,大致会经过几步。
QueryEngine.ask()
-> fetchSystemPromptParts() // 获取默认 Prompt、系统上下文、用户上下文
-> buildEffectiveSystemPrompt() // 根据优先级决定最终 Prompt
-> query() // 调用模型 API
fetchSystemPromptParts() 会并行准备三类内容:
| 组件 | 来源 | 作用 |
|---|---|---|
defaultSystemPrompt | constants/prompts.ts 中的 getSystemPrompt() | 构建标准 System Prompt |
systemContext | context.ts 中的 getSystemContext() | 注入 Git 状态等系统信息 |
userContext | context.ts 中的 getUserContext() | 注入 CLAUDE.md 和当前日期等用户上下文 |
其中最核心的是 getSystemPrompt()。它把 Prompt 分成静态部分和动态部分,中间使用一个边界标记区分缓存区域。
[
// 静态部分:适合全局缓存
getSimpleIntroSection(),
getSimpleSystemSection(),
getSimpleDoingTasksSection(),
getActionsSection(),
getUsingYourToolsSection(),
getSimpleToneAndStyleSection(),
getOutputEfficiencySection(),
// 动态边界
"__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__",
// 动态部分:随用户、环境、会话变化
session_guidance,
memory,
ant_model_override,
env_info_simple,
language,
output_style,
mcp_instructions,
scratchpad,
frc,
summarize_tool_results,
numeric_length_anchors,
token_budget,
brief,
]
2.1 静态 Prompt:建立 Agent 的基本行为
静态部分相当于 Claude Code 的“行为宪法”,每次请求都会包含。它不依赖具体用户或具体项目,因此适合放进缓存前缀。
| 模块 | 作用 | 关键约束 |
|---|---|---|
| 身份介绍 | 声明自己是帮助用户完成软件工程任务的交互式 Agent | 聚焦授权范围内的软件工程与安全测试 |
| 系统行为规则 | 规定输出、工具结果、Hook 反馈、Prompt Injection 识别方式 | 工具结果可能来自外部来源,需要警惕注入攻击 |
| 任务执行指南 | 约束代码修改方式 | 修改文件前先读文件,避免无关重构,不做过度抽象 |
| 操作安全守则 | 约束高风险行为 | 删除、强推、改 CI/CD、影响共享系统前要确认 |
| 工具使用指南 | 指导模型优先使用专用工具 | 读文件用 Read,改文件用 Edit,搜文件用 Glob/Grep |
| 语气和风格 | 控制用户可见输出 | 简洁、少废话、引用代码时带文件路径和行号 |
| 输出效率 | 限制解释长度 | 直奔任务,少写过程性废话 |
这些规则解决的是 Coding Agent 的常见失误:
- 没读代码就开始改;
- 用户只要修 bug,Agent 顺手做了大规模重构;
- 遇到测试失败,不分析原因就换方案;
- 用
rm -rf、git reset --hard等方式绕过问题; - 明明有专用读写工具,却用 Bash 拼命令。
一个可靠的 Coding Agent 必须默认保守。它可以主动执行本地、可逆、低风险的操作,但对影响共享状态、难以回滚或可能破坏用户工作区的动作,应该主动停下来请求确认。
2.2 动态 Prompt:把当前会话的信息拼进去
动态部分会根据用户配置、项目环境和会话状态生成。
| 动态模块 | 作用 |
|---|---|
| 会话指导 | 根据当前启用的工具生成使用说明,例如是否允许询问用户、是否支持 Skill、是否支持 Agent |
| 自动记忆 | 加载持久化记忆,让 Agent 记住用户偏好和项目约定 |
| 环境信息 | 注入工作目录、Git 仓库状态、操作系统、Shell、模型信息等 |
| 语言偏好 | 用户设置了中文、英文等回复语言时,在此处约束 |
| 输出风格 | 注入用户自定义的输出风格 |
| MCP 指令 | MCP(Model Context Protocol,模型上下文协议)服务器提供工具说明时,统一挂载 |
| Scratchpad | 指定临时文件目录,避免把中间产物写到项目目录或系统 /tmp |
| 工具结果清理 | 告诉模型旧工具结果可能被清理,需要主动记录关键信息 |
| Token 预算 | 用户指定消耗目标时,引导模型按目标规划工作量 |
环境信息通常会长成这样:
# Environment
You have been invoked in the following environment:
- Primary working directory: /path/to/project
- Is a git repository: true
- Platform: darwin
- Shell: zsh
- OS Version: Darwin 24.5.0
- Assistant knowledge cutoff is ...
语言偏好则可以是:
# Language
Always respond in Chinese.
Use Chinese for all explanations, comments, and communications with the user.
Technical terms and code identifiers should remain in their original form.
这些信息不应该写死在静态 Prompt 里,因为它们和具体运行环境强相关。把它们做成动态模块,才能在不同用户、不同项目、不同工具配置之间复用同一套 Agent 内核。
2.3 Prompt 优先级:允许覆盖,但要有秩序
Claude Code 会通过 buildEffectiveSystemPrompt() 决定最终使用哪套 Prompt。优先级大致如下:
| 优先级 | Prompt 类型 | 行为 |
|---|---|---|
| 1 | overrideSystemPrompt | 强制覆盖,忽略其他 Prompt |
| 2 | Coordinator Prompt | 协调器模式专用 Prompt |
| 3 | Agent Prompt | 用户定义 Agent 的专属 Prompt |
| 4 | customSystemPrompt | 通过命令行参数传入的自定义 Prompt |
| 5 | defaultSystemPrompt | 标准默认 Prompt |
另外,appendSystemPrompt 通常会追加到最后,除非处于强制覆盖模式。
这个设计很重要。Agent 系统一定会遇到“默认行为不够用”的情况,比如企业内部要加合规规则,团队要定义专用 Agent,某次任务要进入特殊循环模式。如果没有清晰的优先级,多个 Prompt 源叠在一起后就会互相打架。
2.4 缓存边界:把稳定前缀和动态内容分开
大模型 API 通常支持 Prompt Cache 或 KV Cache(键值缓存)。如果每次请求的前缀完全相同,就可以复用缓存,降低延迟和成本。
Claude Code 会把 System Prompt 拆成类似这样的结构:
[
{ text: "x-anthropic-billing-header: ...", cacheScope: null },
{ text: "You are Claude Code...", cacheScope: "org" },
{ text: "静态内容(边界前)", cacheScope: "global" },
{ text: "动态内容(边界后)", cacheScope: null },
]
静态内容放在边界之前,动态内容放在边界之后。这样同一组织、同一版本的静态 Prompt 可以稳定命中缓存,而用户目录、Git 状态、语言偏好这类高变内容不会破坏缓存前缀。
三、给子 Agent 的 Prompt:不是“派任务”那么简单
多 Agent 架构的难点不只是创建多个 Agent,而是主 Agent 如何把任务交给子 Agent。
如果任务描述太短,子 Agent 不知道目标、边界和输出格式;如果任务描述太长,又会浪费上下文,甚至把主 Agent 的不确定性传递给子 Agent。Claude Code 的 AgentTool Prompt 本质上是给主 Agent 的“派工手册”,指导它如何使用子 Agent。
主 Agent 至少要知道四件事:
| 问题 | 设计要求 |
|---|---|
| 有哪些子 Agent | 每个 Agent 的职责、工具权限、模型和适用场景必须清楚 |
| 什么时候不该委派 | 简单搜索直接用 Glob/Grep,不要为了委派而委派 |
| 如何写任务说明 | 任务目标、范围、约束、期望输出必须明确 |
| 如何避免重复工作 | 已委派给子 Agent 的搜索,主 Agent 不应再做一遍 |
多 Agent 的价值主要体现在隔离和并行:子 Agent 可以消化大量工具输出,只把结构化结果带回主上下文,从而减少主 Agent 的上下文污染。
四、Context Engineering:让模型看到正确的信息
Coding Agent 的上下文里会混杂很多东西:用户需求、代码片段、工具输出、错误日志、测试结果、项目规则、历史总结、记忆条目。上下文工程的目标不是“尽量多塞”,而是在 token 预算内保留最有用的信息。
Claude Code 的上下文管理有几个关键机制:
CLAUDE.md提供项目级和用户级指令;- 三层压缩机制控制长对话膨胀;
- Memdir 结构化记忆系统保存长期知识;
<system-reminder>统一包装系统注入信息。
五、CLAUDE.md:项目说明书和行为规范
CLAUDE.md 是 Claude Code 里的重要上下文入口。它通常用来描述项目架构、开发规范、常用命令、测试方式和特殊约束。
一个实用的 CLAUDE.md 可以写成这样:
# Project Overview
This repository is a TypeScript monorepo.
## Commands
- Install dependencies: pnpm install
- Run tests: pnpm test
- Run type check: pnpm typecheck
- Start dev server: pnpm dev
## Code Style
- Prefer existing utilities over creating new abstractions.
- Use functional React components.
- API routes are implemented under apps/api/src/routes.
- Do not modify generated files under src/generated.
## Testing
- Unit tests use Vitest.
- Integration tests require a local PostgreSQL instance.
Claude Code 会把这些内容作为特殊上下文插入对话前部,让模型在后续任务中遵守。
不同位置的 CLAUDE.md 适合放不同类型的信息。
| 路径 | 适合内容 | 是否适合提交到 Git |
|---|---|---|
~/.claude/CLAUDE.md | 个人全局偏好,例如回复语言、代码风格 | 通常不提交 |
项目根目录 CLAUDE.md | 团队共享的项目规范、架构说明、构建命令 | 适合提交 |
CLAUDE.local.md | 个人私有指令、测试账号、本地环境说明 | 不应提交 |
.claude/rules/*.md | 按文件类型、路径或业务领域拆分的细粒度规则 | 视团队策略而定 |
这种 Markdown 驱动的上下文设计很适合 Coding Agent。因为代码项目本身就依赖文件系统组织知识,把项目说明、约束和规则写成可版本化的 Markdown 文件,比把所有规则藏在产品配置里更透明。
六、三层上下文压缩:从便宜到昂贵逐级升级
长程任务的最大敌人是上下文窗口。随着工具调用增多,日志、代码片段和历史消息会快速消耗 token。当剩余空间不足时,Agent 要么报错,要么遗忘关键细节。
Claude Code 使用三层压缩策略,按照成本从低到高逐级处理。
flowchart TD
A[上下文持续增长] --> B{剩余 token 是否低于安全水位}
B -- 否 --> C[继续执行]
B -- 是 --> D[MicroCompact]
D --> E{是否足够}
E -- 是 --> C
E -- 否 --> F[Session Memory Compact]
F --> G{是否足够}
G -- 是 --> C
G -- 否 --> H[Full LLM Compact]
H --> C
| 压缩层 | 是否调用 LLM | 成本 | 适用对象 | 特点 |
|---|---|---|---|---|
| MicroCompact | 否 | 最低 | 工具输出、旧消息 | 规则驱动,速度快 |
| Session Memory Compact | 否 | 低 | 已有会话记忆 | 复用已有摘要 |
| Full LLM Compact | 是 | 高 | 整段会话历史 | 质量高,适合复杂长任务 |
6.1 MicroCompact:规则驱动的第一道防线
MicroCompact 不依赖模型总结,而是用规则压缩工具输出。
它通常只处理容易变长、结构相对明确的工具,例如:
BashReadGrepGlob
而 Edit、Write 这类会改变项目状态的工具输出更关键,通常需要完整保留,避免后续判断丢失依据。
这种策略的工程价值很明显:大量 token 浪费来自搜索结果、日志和文件片段。先压缩这些低风险内容,可以用很小代价释放上下文空间。
对图片等多模态内容,Claude Code 采用固定 token 估算,例如统一按 2000 token 估算。这是典型工程近似:不追求绝对准确,但足够支撑调度决策。
6.2 Session Memory Compact:复用已有会话记忆
如果系统已经生成过会话记忆,就可以用这些记忆替换冗长历史,而不必再次调用 LLM。
常见触发条件包括:
- 当前上下文 token 数达到一定门槛,例如 ≥ 10,000;
- 文本消息数量达到一定门槛,例如 ≥ 5;
- 旧消息可被替换;
- 最近几轮消息必须保留。
保留最近消息很重要。Coding Agent 往往依赖最近几步工具结果继续执行,如果把刚刚失败的命令、刚刚读过的代码或用户刚补充的约束压缩掉,模型会立刻失去任务现场感。
6.3 Full LLM Compact:用模型生成结构化摘要
当规则压缩和会话记忆都不够时,系统才调用 LLM 做完整压缩。
这一步不是简单地让模型“总结一下”,而是强制输出结构化摘要。一个典型模板包含:
1. Primary Request and Intent
2. Key Technical Concepts
3. Files and Code Sections
4. Errors and fixes
5. Problem Solving
6. All user messages
7. Pending Tasks
8. Current Work
9. Optional Next Step
这种结构可以防止摘要遗漏关键事项。对 Coding Agent 来说,最重要的不只是“聊过什么”,还包括:
- 用户的真实目标是什么;
- 哪些文件被读过或改过;
- 遇到过什么错误,怎么修;
- 当前卡在哪里;
- 还有哪些待办;
- 下一步最合理的动作是什么。
压缩阶段还需要禁止工具调用。因为压缩是上下文管理动作,不是任务执行动作。如果模型在压缩时又去读文件、跑命令,会引入副作用,还可能浪费仅剩的上下文机会。
七、Memdir:结构化长期记忆
除了会话内压缩,Claude Code 还需要跨会话保存有价值的信息。Memdir 是一种结构化记忆机制,把长期信息拆成不同类型。
| 记忆类型 | 内容 | 价值 |
|---|---|---|
| User | 用户偏好、沟通方式、常用习惯 | 让 Agent 对齐个人风格 |
| Feedback | 用户纠正过的错误、行为反馈 | 避免重复犯错 |
| Project | 技术选型、架构决策、项目约束 | 保持项目理解一致 |
| Reference | 常用文档片段、代码模式、参考资料 | 提供可复用知识 |
记忆加载不能简单地把所有文件塞进 Prompt。Claude Code 的 loadMemoryPrompt() 更像一个过滤器:
flowchart LR
A[扫描记忆目录] --> B[按类型归类]
B --> C[应用 token 预算]
C --> D[选择高价值记忆]
D --> E[格式化为 Memory Prompt]
E --> F[注入动态 Prompt]
当记忆规模变大时,单靠关键词检索不够。Claude Code 还会引入 LLM-in-the-loop,也就是让模型参与记忆筛选。候选记忆先被收集,再由较便宜的模型判断语义相关性,并限制返回数量,例如最多 5 条。
这个设计平衡了两个目标:
- 召回足够相关的信息;
- 不让记忆把上下文窗口挤爆。
八、Harness Engineering:在模型外部建立约束系统
Harness Engineering 关注的是模型外部的运行环境。它不再只是“提示模型要小心”,而是用权限、沙箱、钩子、事件循环、子 Agent、验证器等机制,把 Agent 的行为限制在可控范围内。
可以把它理解成一套围绕 LLM 的操作系统:
flowchart TD
A[用户请求] --> B[消息规范化]
B --> C[Prompt 与 Context 组装]
C --> D[LLM 推理]
D --> E{是否请求工具调用}
E -- 否 --> F[返回用户]
E -- 是 --> G[权限引擎]
G --> H{Allow / Ask / Deny}
H -- Deny --> I[阻断并反馈]
H -- Ask --> J[请求用户确认]
H -- Allow --> K[Hook 前置拦截]
K --> L[沙箱或本地执行]
L --> M[Hook 后置处理]
M --> N[工具结果注入上下文]
N --> D
九、<system-reminder>:系统级提醒注入
Claude Code 会把系统注入的元信息统一包裹在 <system-reminder>...</system-reminder> 标签中。
例如:
<system-reminder>
As you answer the user's questions, you can use the following context:
# claudeMd
...
# currentDate
Today's date is 2026-06-07.
IMPORTANT: this context may or may not be relevant to your tasks.
You should not respond to this context unless it is highly relevant to your task.
</system-reminder>
这样做有两个好处:
- 明确区分用户自然语言输入和系统注入信息;
- 让工具结果、Hook 反馈、项目规则、日期、能力列表等内容拥有统一格式。
这类提醒会出现在多个阶段:
| 注入场景 | 内容 |
|---|---|
| 用户上下文初始化 | CLAUDE.md、日期、项目说明 |
| 工具结果反馈 | 文件内容、命令输出、搜索结果 |
| Hook 反馈 | 外部脚本返回的阻断原因、修正建议 |
| 能力描述 | 可用 Skill、可用 Agent、任务状态 |
真正的关键在于:这些注入不是随手拼字符串,而是在消息规范化流程里统一处理。这样可以减少格式不一致导致的误解,也方便后续扩展新的注入类型。
十、内置 Agent:用角色隔离任务、权限和成本
Claude Code 内置了多个子 Agent。它们不是为了“看起来智能”,而是为了拆分职责、隔离权限、降低成本、保护主上下文。
| Agent | 角色 | 工具权限 | 模型策略 | 适合场景 |
|---|---|---|---|---|
| General-Purpose Agent | 通用执行者 | 通常拥有全部工具 | 默认子 Agent 模型 | 多步骤搜索、调查、执行任务 |
| Explore Agent | 代码库侦察兵 | 只读 | 便宜、快速模型 | 大范围代码搜索、上下文探索 |
| Plan Agent | 方案设计者 | 只读 | 继承主模型 | 大改动前制定计划 |
| Verification Agent | 质量检验官 | 只读,禁止改项目文件 | 高质量验证模型 | 独立验证实现是否真的可用 |
| Claude Code Guide Agent | 使用说明助手 | 查文档相关工具 | 便宜模型 | 回答 Claude Code 使用问题 |
| Statusline Setup Agent | 状态栏配置助手 | Read、Edit | 中等能力模型 | 修改终端状态栏配置 |
| Fork Sub Agent | 主 Agent 分身 | 继承上下文 | 共享 Prompt Cache | 需要完整上下文但又想隔离执行 |
10.1 Explore Agent:把搜索过程留在子上下文里
主 Agent 搜索代码库时,常常会产生大量中间结果。Explore Agent 的价值是把这些中间结果隔离掉。
它通常具备几个特点:
- 严格只读;
- 不加载
CLAUDE.md,减少无关上下文; - 鼓励并行搜索;
- 只把最终结论带回主 Agent。
如果只是找一个确定文件或函数,直接用 Glob/Grep 更快;如果任务需要多轮探索、超过几次查询,Explore Agent 才更合适。
10.2 Plan Agent:改代码前先做只读规划
Plan Agent 适合大规模修改前的架构分析。它不直接改文件,而是先读代码、找相似模式、识别关键文件、输出实施计划。
一个好的计划输出应该包含:
### Critical Files for Implementation
List 3-5 files most critical for implementing this plan:
- path/to/file1.ts
- path/to/file2.ts
- path/to/file3.ts
这能让主 Agent 后续修改时更聚焦,减少一边改一边探索造成的上下文浪费。
10.3 Verification Agent:不要让实现者自己给自己打分
Verification Agent 的职责不是确认“看起来没问题”,而是主动找问题。它体现了 Harness Engineering 的核心思想:模型可以写代码,但验证环节应该独立、只读、带攻击性。
它的验证方式按变更类型区分:
| 变更类型 | 验证策略 |
|---|---|
| 前端变更 | 启动开发服务器,使用浏览器自动化检查页面和交互 |
| 后端/API | 启动服务,用 curl 或测试客户端验证端点、响应结构和错误处理 |
| CLI/脚本 | 用代表性输入运行,检查 stdout、stderr 和退出码 |
| 基础设施 | 执行语法检查、dry-run,例如 terraform plan、kubectl --dry-run |
| Bug 修复 | 先复现问题,再验证修复,再做回归测试 |
| 数据库迁移 | 运行迁移,检查 schema,验证回滚能力 |
| 重构 | 不改测试的前提下运行现有测试,检查公共 API 差异 |
| 移动端 | 清理构建、安装模拟器、检查 UI 树和点击路径 |
它还需要避免几种 AI 常见偷懒方式:
| 偷懒话术 | 正确做法 |
|---|---|
| “代码看起来是对的” | 运行它 |
| “测试已经通过了” | 独立验证,不只相信实现者的测试 |
| “大概没问题” | “大概”不等于验证 |
| “先看看代码” | 该打端点就打端点,该点 UI 就点 UI |
| “太耗时” | 验证范围由任务风险决定,不由模型主观决定 |
Verification Agent 只能看,不能改项目文件。这个限制很关键:验证者不能一边发现问题一边修,否则验证职责会重新和实现职责混在一起。
十一、权限引擎:Allow、Ask、Deny 三行为模型
Agent 调工具前,必须经过权限判断。Claude Code 的权限体系可以抽象成三种动作。
| 动作 | 含义 | 示例 |
|---|---|---|
| Allow | 自动允许 | 读文件、搜索文件、运行低风险本地检查 |
| Ask | 请求用户确认 | 推送代码、删除文件、修改共享配置 |
| Deny | 自动拒绝 | 明确危险或违反规则的操作 |
权限规则可以来自多个来源,例如全局配置、CLI 参数、命令行规则、会话级规则。越靠近当前会话的规则通常越具体,也更适合临时覆盖默认行为。
一个简化的权限判断可以写成:
type PermissionDecision = "allow" | "ask" | "deny";
function decideToolUse(toolName: string, input: unknown): PermissionDecision {
if (matchesDenyRule(toolName, input)) return "deny";
if (matchesAllowRule(toolName, input)) return "allow";
return "ask";
}
这个模型虽然简单,但对 Agent 安全非常重要。只靠 Prompt 告诉模型“不要做危险操作”是不够的,高风险动作必须在模型外部被拦截。
十二、沙箱隔离:权限放行后仍然要防误伤
权限引擎解决的是“是否允许做”,沙箱解决的是“即使允许,也要限制影响范围”。
在 Linux 环境下,可以用 bubblewrap 这类工具构建轻量级沙箱。典型能力包括:
| 隔离能力 | 作用 |
|---|---|
| 文件系统隔离 | 根目录只读挂载,只开放白名单路径 |
| 网络隔离 | 限制网络访问,避免未知外连 |
| 进程隔离 | 使用独立 PID 命名空间,减少进程影响 |
| 用户降权 | 用非 root 用户执行,降低提权风险 |
沙箱不适合一刀切。有些命令需要交互式终端、特殊网络能力或系统级访问,强行放进沙箱反而会导致任务失败。因此系统需要类似 shouldUseSandbox() 的决策逻辑:能隔离就隔离,不兼容时回到权限引擎和用户确认。
十三、异步生成器主循环:让 Agent 可流式、可暂停、可取消
成熟 Agent 不应该是一个巨大同步函数。Claude Code 的主循环可以抽象成 async function*,也就是异步生成器。
async function* queryLoop(messages: Message[]) {
while (true) {
const normalized = normalizeMessages(messages);
const response = await callLLM(normalized);
yield {
type: "model_response",
response,
};
const toolCalls = parseToolCalls(response);
if (toolCalls.length === 0) {
return response;
}
for (const toolCall of toolCalls) {
const decision = await checkPermission(toolCall);
if (decision === "ask") {
yield { type: "permission_required", toolCall };
continue;
}
if (decision === "deny") {
yield { type: "tool_denied", toolCall };
continue;
}
const result = await executeTool(toolCall);
yield {
type: "tool_result",
toolCall,
result,
};
messages.push(resultToMessage(result));
}
}
}
这种结构带来几个直接好处:
| 能力 | 说明 |
|---|---|
| 流式反馈 | 思考、工具调用、文件读取、错误都能逐步 yield 给上层 |
| 协作式控制 | 上层可以在权限确认点暂停执行 |
| 优雅取消 | 用户中断时可以停止迭代并清理资源 |
| 状态保持 | 多次 yield 之间可以保留局部状态 |
主循环还需要处理异常恢复:
| 异常 | 恢复策略 |
|---|---|
prompt-too-long | 触发上下文压缩,从轻量压缩逐步升级 |
max-output-tokens | 自动发送 continue,限制续写次数 |
| 网络波动 | 指数退避重试 |
| 工具失败 | 读取错误信息,诊断原因后再决定是否换方案 |
Agent 的可靠性很大程度来自这类循环控制。模型回答只是其中一步,真正的执行系统要能处理各种不完美情况。
十四、Hooks:把 Agent 运行过程变成可编程事件流
Hook 是 Harness Engineering 里非常实用的一层。它允许开发者在工具调用、消息提交、文件编辑、会话开始结束等节点插入自定义逻辑。
| 生命周期 | Hook | 触发时机 |
|---|---|---|
| 工具生命周期 | PreToolUse | 工具调用前 |
| 工具生命周期 | PostToolUse | 工具调用后 |
| 工具生命周期 | ToolError | 工具执行出错 |
| 会话生命周期 | SessionStart | 会话开始 |
| 会话生命周期 | SessionEnd | 会话结束 |
| 会话生命周期 | SessionPause | 会话暂停 |
| 会话生命周期 | SessionResume | 会话恢复 |
| 消息生命周期 | PreSampling | 模型采样前 |
| 消息生命周期 | PostSampling | 模型采样后 |
| 消息生命周期 | UserPromptSubmit | 用户提交输入 |
| 文件操作 | PreFileEdit | 文件编辑前 |
| 文件操作 | PostFileEdit | 文件编辑后 |
| 文件操作 | PreFileWrite | 文件写入前 |
| 文件操作 | PostFileWrite | 文件写入后 |
Hook 不只是监听,还可以干预。一个 Hook 可以返回结构化 JSON:
{
"blocked": true,
"reason": "Do not edit generated files under src/generated."
}
也可以修改输入:
{
"input": {
"path": "apps/web/src/App.tsx"
}
}
还可以注入反馈:
{
"message": "Lint check failed. Please fix formatting before continuing."
}
一个声明式 Hook 配置可以类似这样:
{
"hooks": [
{
"event": "PreToolUse",
"match": {
"tool": "Edit"
},
"command": "node scripts/check-edit-policy.js"
}
]
}
Hook 机制的风险也很明显:外部脚本可能卡住、死循环或网络阻塞。因此必须设置超时,例如 10 分钟后强制终止 Hook。否则一个坏插件就能拖垮整个 Agent。
十五、几个可复用的工程细节
Claude Code 还有一些不直接决定推理能力、但能提升稳定性和产品体验的工程细节。
15.1 防止电脑休眠
在 macOS 上,系统可以调用 caffeinate 防止空闲休眠。更稳妥的做法不是设置一个超长超时,而是设置较短有效期并周期性续期。
原因很简单:如果主进程被强杀,清理回调可能不会执行。短超时可以保证辅助进程最终自动退出,不会让电脑一直保持唤醒。
15.2 蒸馏防护
如果一个 Agent 的完整工具调用链、思考痕迹和中间输出全部暴露,很容易被用来训练模仿模型。蒸馏防护可以从两处入手:
| 策略 | 作用 |
|---|---|
| 注入假工具定义 | 让复制输入输出的训练数据混入不可用工具 |
| 精简工具过程输出 | 对外只展示汇总进度,不暴露完整调用参数和链路 |
对 SDK(Software Development Kit,软件开发工具包)用户来说,精简输出还能降低噪音;对系统来说,也减少了核心行为链路被完整复刻的风险。
15.3 内部灰度与 Dogfooding
Dogfooding 指团队内部长期使用自己开发的产品,并用真实任务暴露问题。工程上可以通过用户类型、环境变量或特性开关区分内部功能和外部功能。
这种机制适合用于:
- A/B 测试新 Agent;
- 验证新压缩策略;
- 试运行更严格的验证流程;
- 收集失败案例并改进 Prompt。
15.4 负面反馈触发调查
当用户输入明显负面情绪时,系统可以触发反馈收集,而不是简单忽略。这里的关键不是“识别用户在骂什么”,而是把挫败时刻转化成可诊断数据:哪一步失败、哪个工具误用、哪个回答让用户不满意。
这种设计适合内部测试阶段,用来定位 Agent 的真实痛点。
十六、从 Claude Code 提炼出的 Agent 设计原则
Claude Code 的设计可以抽象成几条可复用原则。
1. Prompt 要模块化,而不是写成一大段
静态规则、动态环境、用户偏好、工具说明、MCP 指令、记忆内容应该分层组织。这样才能缓存、覆盖、调试和扩展。
2. 上下文要有预算意识
不是所有历史都值得保留。工具输出、旧消息、会话记忆、长期记忆应该按照价值和成本进入上下文。压缩也要分级,能用规则解决就不要调用 LLM。
3. 工具权限必须在模型外部控制
Prompt 可以提醒模型小心,但不能代替权限系统。高风险动作需要 Allow、Ask、Deny 这样的外部判定。
4. 子 Agent 要承担隔离职责
子 Agent 的价值不只是并行,更是上下文隔离、权限隔离和成本隔离。Explore、Plan、Verification 这类角色拆分,比一个万能 Agent 从头做到尾更稳。
5. 验证必须独立
实现者不能自己给自己打分。Verification Agent 应该只读、独立、按变更类型运行真实验证,而不是只看代码。
6. 主循环要可观测、可中断、可恢复
复杂 Agent 一定会遇到工具失败、上下文超长、网络抖动、用户取消和权限等待。异步生成器、事件流、重试、压缩和 Hook 是生产级 Agent 的基础设施。
7. Harness 决定 Agent 能不能进生产
Prompt 决定模型怎么想,Context 决定模型知道什么,Harness 决定系统能否安全执行。真正的 Agent 工程化,不是让模型自由发挥,而是给模型一套可控的运行轨道。