动态构建系统提示词
上一课的分段提示词是硬编码的。当只有一个项目、一个沙箱和一个固定工具集时,这没问题。只要这些人动了,提示词就得跟着动。
不同的工作目录。后端沙箱不同。一个只有read和grep的子 Agent。硬编码字符串无法承载这些内容。函数可以。
学习成果
src/system.ts中的buildSystemPrompt(context)函数返回了类型PromptContext的系统提示词字符串。Agent的 instructions 现在是从运行时状态派生的,而不是粘贴进去的。
快速路径
- 创建
src/system.ts,配备PromptContext接口和buildSystemPrompt(ctx)函数 - 从多个章节组成提示词,包括选修部分如
gitBranch和projectContext - 调用
buildSystemPrompt(...)在index.ts中,并将结果传instructions
动手练习 3.2
把提示词提取到一个类型化的构建器中。
要求:
- 定义
PromptContext,包含workingDirectory、sandboxType、toolNames、可选gitBranch、可选projectContext - 写入返回分段提示词的
buildSystemPrompt(ctx: PromptContext): string - 将
gitBranch和projectContext设为有条件,只有在设置时才包含它们的部分 - 在
index.ts中,构建上下文对象,并为instructions字段调用buildSystemPrompt(ctx)
实现提示:
- 把部分推入数组,最后
join("\n")。纯字符串 concat,无模板引擎 - 条件段使用简单的
if (ctx.foo) sections.push(...)。不要追求更花哨的模式 - 保持
buildSystemPrompt纯净。同样的情境,同样的环境,提示词出,没有副作用。这使得它可以进行单元测试
语境的形态
提示词取决于运行时状态。把这些状态装瓶成一个整体:
export interface PromptContext {
workingDirectory: string;
sandboxType: string;
toolNames: string[];
gitBranch?: string;
projectContext?: string;
}workingDirectory和sandboxType总是适用。toolNames让提示词列出实际接线的工具(这在给子 Agent子集时很重要)。gitBranch和projectContext是可选的,因为它们并不总是可知的。
构建器
export function buildSystemPrompt(ctx: PromptContext): string {
const sections: string[] = [];
sections.push(`You are a coding agent working in: ${ctx.workingDirectory}`);
sections.push(`Sandbox: ${ctx.sandboxType}`);
sections.push(`
# Agency
- USE your tools. Read files, search code, run commands, then answer.
- Do NOT explain what you WOULD do. Actually do it.
- Available tools: ${ctx.toolNames.join(", ")}`);
if (ctx.gitBranch) {
sections.push(`- Current branch: ${ctx.gitBranch}`);
}
sections.push(`
# Guardrails
- Prefer simple, minimal changes
- Search before creating, and reuse existing patterns
- No new dependencies without asking`);
if (ctx.projectContext) {
sections.push(`
# Project Instructions (from AGENTS.md)
${ctx.projectContext}`);
}
return sections.join("\n");
}这里没有模板引擎。没有什么DSL。有一个阵列,几个push 调用,还有一个join。这是故意的。提示词是一根绳子。构建它应该看起来像搭一根绳子。
接入主流程
index.ts,将内联instructions的字面值替换为建调用:
import { buildSystemPrompt } from "./src/system";
const instructions = buildSystemPrompt({
workingDirectory: cwd,
sandboxType: "local",
toolNames: Object.keys({ read, grep, bash }),
});
const agent = new ToolLoopAgent({
model: "anthropic/claude-haiku-4-5",
instructions,
tools: { read, grep, bash },
stopWhen: stepCountIs(10),
});Agent在单一任务上的行为可能看起来和以前一样。这种胜利是结构性的。添加 git 上下文行、交换沙箱类型或剥离子 Agent的部分,现在都需要编辑一个专注于函数的函数,而不是在多行字符串中查找并替换。
**注意:为什么是函数,而不是字符串**
提示词是Harness最重要的配置。把它设为函数意味着它可测试(断言输出在特定上下文下的样子)、可组合(添加部分而不影响其他部分)、可替换(用户可以自己构建构建器)、确定性(每次都相同的上下文、相同的提示词)。费用是一份文件。好处是在你第三次添加某个部分时才显现出来的。
动手试试
运行你以前用过的任何一个提示词:
bun run index.ts . "Find all TODO comments in this project"输出应该和上一节课一样,因为提示词内容是一样的。这种变化是内心的。通过记录一次该提示词来确认Agent是否还具备所需工具:
console.log(instructions);你应该能看到完整的Agent和护栏部分,并插值了工作目录和工具名称。
npx tsc --noEmit提交
git add src/system.ts index.ts
git commit -m "refactor(prompt): extract buildSystemPrompt with runtime context"完成标准
- [ ]
src/system.ts出口PromptContext和buildSystemPrompt - [ ]
buildSystemPrompt在相同的上下文下返回与上一课相同的提示词内容 - [ ]
gitBranch和projectContext为可选,仅在提供时才包含 - [ ]
index.ts调用buildSystemPrompt(...)代替使用内联字符串 - [ ]
npx tsc --noEmit
**注意:写个测试给提示词**
添加一个快速断言:用gitBranch: "main"构建提示词,并确认输出中包含“当前分支:主”。不gitBranch地构建,确认这条线路不存在。这是提示词中最小的单元测试,它捕捉到了那种几乎无法通过读取模型输出发现的漏洞。
参考实现
export interface PromptContext {
workingDirectory: string;
sandboxType: string;
toolNames: string[];
gitBranch?: string;
projectContext?: string;
}
export function buildSystemPrompt(ctx: PromptContext): string {
const sections: string[] = [];
sections.push(`You are a coding agent working in: ${ctx.workingDirectory}`);
sections.push(`Sandbox: ${ctx.sandboxType}`);
sections.push(`
# Agency
- USE your tools. Read files, search code, run commands, then answer.
- Do NOT explain what you WOULD do. Actually do it.
- Available tools: ${ctx.toolNames.join(", ")}`);
if (ctx.gitBranch) {
sections.push(`- Current branch: ${ctx.gitBranch}`);
}
sections.push(`
# Guardrails
- Prefer simple, minimal changes
- Search before creating, and reuse existing patterns
- No new dependencies without asking`);
if (ctx.projectContext) {
sections.push(`
# Project Instructions (from AGENTS.md)
${ctx.projectContext}`);
}
return sections.join("\n");
}import { buildSystemPrompt } from "./src/system";
const tools = { read, grep, bash };
const agent = new ToolLoopAgent({
model: "anthropic/claude-haiku-4-5",
instructions: buildSystemPrompt({
workingDirectory: cwd,
sandboxType: "local",
toolNames: Object.keys(tools),
}),
tools,
stopWhen: stepCountIs(10),
});