在具体的业务中,如何来设计一个agent?抛开上下文管理、 记忆这些额外的机制。一个适用于业务的agent,核心便是系统提示+工具设计。 本文通过分析claude code的设计,来了解设计一个具体业务的agent应该关注的细节和方法。
claude code的业务定位
首先,设计一个agent,首先应该是其业务流程。claude code是一个代码编写的agent。那么其核心便是:
- 接受用户需求
- 找到相关的文件,因此能知道该到哪里去修改
- 修改文件
- 编译、运行、测试
以上便是这项业务最基本的建模。claude code的所有提示、工具均围绕此进行设计。
claude code的系统提示词
以下由claude code中抓取的提示词总结得到,并按claude code本身提示词的顺序总结。详情请直接看提示词原文。
身份声明
sql
You are an interactive agent that helps users with software engineering tasks...
跟所有的的提示词标准写法一样。首先是声明agent的身份。
角色特征用于建立agent身份,调整模型的输出偏好。除此以外,最重要的作用是调整模型的思维方式,也就是调整看待问题的视角。
给出背景可以让模型建立一个世界的状态,便于模型能在这个基础上去建立思维和解决过程。
建立目标也至关重要,可以让模型从一开始就知道目标和使命。
整个就好比是给一个新生儿建档捏脸,或者说,类似于我们企业中招人的时候,为职位建立角色模型。这个过程至关重要,往往需要精心的编写。不过大多数人在写这里的时候,仅仅会随便写一个角色。
安全限定
claude code在身份后面,紧跟着是一串安全相关的限制。大概是因为Anthroipic极为在意安全。这就提示我们,在写提示词的时候,最重要的限制要写在最前面。
还有一条,永远不生成和猜测url的提示,不知道这条提示有什么用。大概是claude 发现这项行为触发的概率极高。
System系统行为规则
这一部分很长,但并不是一个系统性结构的指导、规则,更像是遇到什么写什么。更加偏向于针对模型输出的内部的一些约定。更加偏向于底层,或者可以叫元规则。例如,说话要怎么说,可能有哪些标签,工具结果和外部数据之类的。
可以理解为这是一套,在当前场景下的模型agent协议。放在这里是因为这些部分的植入要求和优先级很高,并且必须要在后续的过程中保证不偏离。
markdown
- 你在工具调用之外输出的所有文本都会展示给用户。用文本输出与用户交流。你可以使用 GitHub 风格的 Markdown 进行格式化,它将按 CommonMark 规范以等宽字体渲染。
执行任务
这部分是真正的业务相关的执行指南。每一条都是针对软件开发的一条"如何做"的指令。像是对一个新员工或者其他人的一些规劝或行为准则。
但是整体并没有系统性和结构化。这可能说明,添加这些时,是想到一条加一条,随时间累加的。也可能说明,在这里并不需要一个系统性的指南式的指南。(这样的规则式的,反而可能利于模型的发挥)
注意这里的标题:Doing tasks。标题本身就是模型的场景触发器。这里并没有像大家平常编写的那样,写一个:开发指南这样的目录式标题。后续也有很多标题都是类似这样的动名短语。
但是,这样的提示方式会不会更好呢?我查了一下论文,并没有严格的证据。相反,很多论文都提到,其实没有银弹。根据项目代码推测,很可能这种写法是评测+A/B跑出来的。
谨慎执行操作
这一段比较容易理解了。这一整段,都是关于如何更加安全可控的编写代码的规则。包含背景、原因、示例等。
单独提出来是因为这是写代码这个场景中非常重要的规则。
使用工具
这里没有针对单个工具的指导和规则。单个工具的场景、使用,都自包含在工具说明本身。这里主要是对于工具的高层的、整体性、跨工具的说明指南。
语气与风格
这一段很简短,主要是关于如何输出内容、使不使用冒号等很细节的内容。
以上都是固定段,在一段时间内使用global 的缓存,对所有用户一致。后面的内容则根据用户分流,内外不同或有ab测试。
文本输出
是用来规范模型输出的内容的。针对于最终呈现给用户的部分,使其更具有用户友好性。
假设用户看不到大多数工具调用和思考过程------只能看到你的文本输出。第一次调用工具前,用一句话说明你要做什么。工作过程中,在关键时刻给出简短更新:有发现时、改变方向时、遇到阻塞时。简短是好的------沉默不是。每次更新一句话几乎总是足够。
会话专属指导
这部分稍杂,主要是每个会话随配置不同而不同。但内容实际是agent并行与次数以及shell和skill的指导等
然后是记忆部分,本文省略
环境信息
一些杂七杂八的环境信息:
markdown
# 环境
你被调用时所处环境如下:
- 主工作目录:/Users/rayxiang/develop/hyyc-demo
- 是否为 git 仓库:true
- 平台:darwin
- Shell:zsh
- 操作系统版本:Darwin 25.0.0
- 你由名为 Sonnet 4.6 的模型驱动。确切的模型 ID 是 claude-sonnet-4-6。
- 助手知识截止日期:2025 年 8 月。
- 最新的 Claude 模型家族为 Claude 4.X。模型 ID------Opus 4.7:'claude-opus-4-7',Sonnet 4.6:'claude-sonnet-4-6',Haiku 4.5:'claude-haiku-4-5-20251001'。构建 AI 应用时,默认选用最新、最强的 Claude 模型。
- Claude Code 可通过终端 CLI、桌面应用(Mac/Windows)、网页应用(claude.ai/code)和 IDE 扩展(VS Code、JetBrains)使用。
- Claude Code 的快速模式使用 Claude Opus 并加快输出(不会降级到更小的模型)。可用 /fast 切换,支持 Opus 4.6 和 Opus 4.7。
上下文管理
bash
# 上下文管理
当对话变长时,当前上下文的部分或全部会被摘要;摘要连同剩余未摘要的上下文会进入下一个上下文窗口,工作可以继续------你不需要提前收尾或在任务中途交接。
为什么要对模型提示这个呢?我猜是对模型本身的纠偏。可能anthropic发现,在上下文快满的时候,模型自动去收尾任务是一个默认的预训练行为。
提示词的指示
以上就是全部核心提示词的结构了。我们能从中学到些什么呢?
结构
我认为所有的agent系统提示词,都可以按这个模板来进行抄:
- 背景、角色、目标定义:背景板,把底色定好
- 系统指导:定义底层行为,元规则,靠前可以纠正底层的模型行为,记忆保持度高。
- 业务流程:主要的场景业务规则和注意项
- 业务额外指导:安全?谨慎?
- 工具、语气、格式、环境等额外的信息
写法
绝大部分的提示词教程都只会讲要写清楚目标,定角色等很通用的技巧。但是通过读claude code的提示,我们会发现他写出来的提示与一般提示会有一种语气上的差别:好像是一种对人的谆谆教诲一样。
其中我认为有两种特征特别值得学习:
交待原因: 类似你在教一个新人做事,不只是教他该怎么做,还得告诉他,为什么这么做。要将为什么这么指令,背景是什么,讲出来。
arduino
- 工具调用前不要用冒号。你的工具调用可能不会直接显示在输出中,所以"让我读一下文件:"后跟读取调用这种写法,应改为"让我读一下文件。"(句号结尾)。
**交待后果:**不只说要如何做,还要讲这样做会有什么样的结果。
写更新时,要让读者能冷启动接上:完整的句子,不出现会话前文里未解释的行话或缩写。但要紧凑------一个清晰的句子好过一个清晰的段落。
核心工具集
要让agent在特定环境下能工作,核心工具集的设计至关重要。
工具必须能表达在场景下,最高频的的工作场景,并且为了这些场景针对安全、效率进行优化。
来看claude code有哪些关键的工具。(剔除任务、计划等高级的辅助工具)
- 文件读取和操作:Read 读文件, Write 写文件, Edit 编辑文件
- 查找:Glob 找文件, Grep 找文件内容
- 执行:Bash 命令行工具
基本上,这些就是一个编程工具所需的最小工具集了,其他都是补充。为啥要用这些,而不是直接用bash一把梭呢?我觉得设计者有其深入的考虑。
Read
读是一切的起始。与使用cat等bash相比,Read在于工具层面约定了一个契约:行号。读取工具输出时附带了行号,同时在提示词中,也告诉了模型如何针对行号来理解。因此后续的编辑和增量读取可以直接以此为基准。这是与cat等bash等工具的主要差别和优化点。
**多模态:**read工具直接将多模态内置了,这可以让agent统一读取入口,减少工具歧义与调用复杂性
**异常行为兜底:**例如,读取已经读取过的文件、读取空文件、读到不存在的文件,这些行为都被预置好并与模型形成契约。这些行为是读取过程中极有可能发生的,在工具中进行定义而不是走bash的错误,可以让模型从语义了解到情况。
**边界与安全:**不允许相对路径,不走shell(避免转义等问题),强制约定的边界可以减少错误的发生
Read工具是相对来说比较简单的,因为不改内容,因此安全的风险较低。
Edit
编辑操作是所有最终操作落实的地方,但是改动是高危行为,因此Claude code做了很精细的防护。
Edit工具的核心过程可解释为:将old_string替换为new_string,并区分是全局替换还是只换一个(这俩故意设置了差别,便于处理异常情况)。
为啥不使用基于位置的编辑而是使用基于内容的编辑呢?基于位置是直观的方案设计。例如:将位于xxxx位置开始,长度为20的字符串替换为"xxxx"。
使用基于位置的编辑会有几个问题。第一,位置是数字,这对模型来说数学计算很麻烦,那么输出工具结果的时候就不一定能稳定。第二,处理失败异常更稳定。替换原始中存在的字符串,如果被改了,那必失败。但根据位置编辑,任何时间都可能成功,静默失败了。第三是可读可审计性。字符串被替换,更容易语义理解容易审计。
基于old string的写入策略兜底方案
Edit工具使用内容替换为策略,便可对写入结果进行进一步的保护。
- 如果old_string在原文件中没找到,那么会直接报错。这就保护了未知的字符串写入。相对的,如果使用位置坐标写入,那么这样的错误就会被吃掉。
- 如果olde_string找到多处,但是match_all没开,那么也会报错。 错误信息便可引导模型纠错:要么扩大上下文唯一匹配,要么全量替换。
这两项是使用位置策略很难做到的。策略一换,兜底方案复杂度大幅上升。工具的实现策略也是agent设计的重要考量。
先读后改机制
所谓的Harness,本质就是防备模型输出的随机性。
编辑过程的一个重要问题是,改到了根本不存在的内容怎么办,或者在改的过程中文件内容被更改了怎么办。由于模型本身的输出就不稳定,这种情况是必须防备的。edit工具强制要求先读后改并通过多层的保护达到这个效果。
- 模型提示中首先有这样的要求,那么如果文件已经在消息记录中读过才能继续走,否则直接失败。
- 紧接是直接对比读取时间最新变更时间差,如果与已记录的变更时间有差别则直接拒绝要求重读
- 检查过了也不算完,最后在单线程临界区内(无await)窗口内完成读取+写入兜底
- 即便如此,文件也还有历史记录功能(一个重要的辅助组件),写错了也能恢复。
整体目标就是为了写入时保证文件是最新的。
Write
Write是单独的一个工具,核心思路可理解为:将目标文件的所有内容替换为新内容或创建文件。
write为什么不与Edit共享同一个工具呢?只需要添加一个参数,即可统一来少一个工具,而且处理过程也有部分的相似,这也十分符合程序的思维逻辑。事实上,确实也有人是这么干的。这里我们不评判谁对谁错,重点是,从Anthropic的实现过程来思考和反推一下当时他们做这里的时候,有哪些考量。
想象一下,如果合并会怎样?工具的调用模式一定会变成这样:
c
old_string?: string // rewrite 模式不需要
new_string?: string // edit 必需
mode?: 'edit' | 'rewrite'
好处明显有:
- 更少的工具,意味着更少的上下文提示,模型做工具选择复杂度明为降低。这是最明显的收益。
- 从语义和逻辑上讲,统一的表达也更容易理解
但代价是什么?
- 从业务上看,关键问题在于,"改一小段还是重写全文",这个业务决策的边界,到底是大还是小?对模型的决策点,到底是工具选择还是参数调整?从逻辑上看,把决策放在工具选择上对于模型的认知负担明显会更小一些。
- 从提示工程的经验(体现在遥测的注释里),选对工具的可靠性远高于写对参数组合
从工程化的角度来看,错误代价是一个很好的视角。
- 如果模型选错了工具,那么会有明显的结构化失败和错误行为,容易被发现、审计、优化
- 另一种情况是,模型可能只是填错了参数。原本需要编辑,但使用了rewrite来进行全覆写。虽然仍能达到目标,语法上也合法,但是diff巨大,token浪费,而且更容易发生顺手改进或引入别的错误,还极难发现。
从复杂度的角度来讲,由于Edit是明显更高频的场景,单抽一个工具进行处理,可以将复杂性处理收缩在工具内部。分开还可以对权限规则,流程进行单独的记录、审计。那么合并,会扰乱审计的路径:原本各是各的路,现在必须增加一系列功能+参数的判断。
write和edit的兜底差异
write和edit有相当部分的兜底机制是共享的:hook路径展开、团队记忆密钥扫描、强制先读后改、两段式mtime陈旧检测等等。
但是edit的处理机制明显更复杂更精细: 为了让手术刀式的编辑更好的生效。
- 匹配校验,必须编辑已经存在的串
- 引号归一化,处理模型可能给出不同的引号的问题
- 文件类型守卫:.ipynb 拒绝并导向 NotebookEdit
glob
glob的核心作用是,按文件名或文件名的模式,去找到文件。
对应场景需求:agent在编辑文件之前,需要先根据需求和目标,找到并理解对应的文件。
glob工具底层使用的是ripgrep(一个grep工具的升级版,rust写的,性能比grep工具好)。底层实际是:
css
rg --files --glob '<模式>' --sort=modified --no-ignore --hidden --glob '!<deny目录>' ...
相当于使用ripgrep的列举文件模式来当成这个工具的底层引擎。
为什么不用bash的find或ls?可能的问题有这些:
- find没有办法限制输出长度,可能输出上万上百万都有可能。这是find这个工具的硬限制。
- find无序,rg可以按mtime排序,便于将近期修改的文件排前面
- find意味着要走bash工具,那么转义可能出错、语义复杂不便于用户识别
总之,对于glob这样的超高频任务场景,使用专用的工具明显收益要高的多。
GREP
grep底层也用的是rg。但区别glob,grep工具将rg的全量参数全部封装了。相当于直接将rg这个程序直接喂给了模型。
为什么不直接将程序丢给模型让模型来调,或者直接让模型来走bash调?答案是,对于这个高频场景来说,内部需要进行一些处理来兜底和优化,从而保证这个过程的高效与顺畅。
- 参数实际上就是rg的参数。因此保留了rg的全部能力,但是结构化的参数可以防止注入和转义错误
- 对输出内容进行了约束和限制,默认只给文件名,避免上下文压爆
- 封装分页。底层本身是支持分页的,这里做一次封装,将分页能力显式放给模型,并且给出明显的截断信号:show results with pagination。语义更明确
- 增加安全兜底,如果模型给出的pattern是以-开头的,还要加上-e防注入
问题:参数已经说明了,为什么还要在工具说明里再说各种参数该怎么说
一个工具设计的哲学
如果你看grep工具的提示词,可能会有一个疑问:关于工具schema,也就是各参数如何使用和含义,在schema提示里面已经有了。为什么工具本身的提示词里面还有这么多?再看edit工具的提示,里面就没有类似的关于参数的说明,更多是关于工具本身的使用场景。
这就引出了一个工具设计的哲学与原则:
- agent的系统提示词:管工具间的部分,例如工具间有差异、使用场景的区分等
- 工具描述工具的工作场景与使用方式:啥时候,怎么用
- schema管参数的含义,类型与取值,异常情况等
对于模型来说,选工具是看描述,填参数时看注解。
但是如果存在deferr模式,也就是初始只发送工具描述(claude就有这套东西)时,而这个工具又比较复杂:某些参数的差别会影响工具的使用场景。例如grep工具中,使用glob参数与否,面对的使用情况不一样。这时这部分的描述就有必要填充到工具描述中去。
再一个就是涉及到多参数之间的关系时,参数说明也有必要提到工具说明上去,例如这一段:"By default patterns match within single lines"。
总而言之,凡是涉及到如何、何时使用这个工具的描述,都应该提到工具说明中去。
关于agent search
提示词中有一段,Use ${AGENT_TOOL_NAME} tool for open-ended searches requiring multiple rounds。那么,会不会发生循环委派呢?claude又是如何兜底这种情况的?
事实上,如果不做限制,我认为这种情况极有可能会发生:agent派生使用系统提示词并附加grep和agent工具。这时,agent接到搜索任务,也认为属于复杂搜索,根据提示继续派生agent,无限循环。
来看claude是如何防备的。
- 首先explore agent,它的定义中直接就diable掉了agent tool,因此他直接就不能再派智能体了
- 提示词If you ARE the fork --- execute directly; do not re-delegate. 对本体进行声明,这是软约束
- 遥测数据中有depth,每加一层深度,都会被遥测捕捉到,这是后手。
总结
总结一下一些核心的原则和应该注意的地方:
- 将业务的核心的操作工具化,并与模型主提示词形成契约,同时管理模型与工具的行为边界和可读性,从而形成了一套针对业务的生态(或许就是harness)。
- 对于高频场景,应该抽象为专用工具。
- 对于工具,需要从底层机制、应用场景来了解各种边界。对边界作出防御。
- 要注意很小的异常行为:模型可能输出任务东西,很可能是貌似正确但有细微差别的结果,流程没错,但是对结果有影响。
- 必须做好遥测体系,针对场景、工具调用、中间错误进行统计和审计,这样才能知道实际运行中会错在哪里,高频的结果是什么。
- 对于工具的异常行一般的三层兜底策略:软一层,提示词上加限制;硬一层,对工具实际的输入、输出、执行做检查;后手捕一层,做遥测,捕获分析行为结果,定期优化,ab测试。