12-factor-agents 实战:用推理步骤自定义 Agent Prompt,让工具调用更可靠
12-factor-agents 实战:用推理步骤自定义 Agent Prompt,让工具调用更可靠
导读
本文是 12-factor-agents 工作坊系列第 6 章(workshops/2025-05/sections/06-customize-prompt/)的完整技术讲解,核心主题是:在 factor-02-own-your-prompts 原则指导下,通过修改 BAML 定义的 prompt,为 Agent 增加显式"推理步骤"(reasoning steps),从而提升工具调用的准确性与可观测性。读完本文,你将掌握开启 BAML 调试日志、定制 Agent prompt、重新生成客户端代码、在日志中观察推理过程,以及将推理结果写入结构化输出等一系列可复现的实战能力。
为什么要"拥有你的 Prompt":Factor 2 背景
在进入第 6 章的实操之前,先明确其理论基础。12-factor-agents 的 Factor 2:Own Your Prompts(详见 content/factor-02-own-your-prompts.md)强调:不要把 prompt 工程外包给框架的黑盒抽象。
许多 Agent 框架提供类似下面的"黑盒"写法,把角色、目标、性格、工具全部封装进 Agent 对象:
agent = Agent(
role="...",
goal="...",
personality="...",
tools=[tool1, tool2, tool3]
)
task = Task(
instructions="...",
expected_output=OutputModel
)
result = agent.run(task)
这种方式的好处是开箱即用、内置了高质量 prompt 工程,但缺点是难以调优、难以逆向工程——你无法精确控制送入模型的每一个 token。本章的做法则完全相反:把 prompt 当作一等公民的代码,直接写在 BAML 源文件里,逐字可控、可测试、可演进。
Factor 2 的核心思想:prompt 是应用逻辑与 LLM 之间的主接口,必须完全掌握在自己手中,而不是交给框架的黑盒。
推理步骤 vs 推理模型:先厘清概念
本章标题为 "Customize Your Prompt with Reasoning",核心是给 prompt 增加推理步骤(reasoning steps),这区别于当前流行的"推理模型"(reasoning models):
- 推理模型:把推理能力内置在模型权重与运行时里(如 OpenAI o 系列),对使用者而言是一个开箱即用的开关,但你无法控制模型"怎么想"。
- 推理步骤:在 prompt 中显式要求模型"先规划再行动",把推理过程写成可观察、可调试、可约束的文字。它不依赖特定模型,普通指令模型(如本工作坊使用的
gpt-4o)也能获得类似效果。
本章采用的就是第二种思路:通过修改 prompt 让模型先列出行动规划,再输出结构化的工具调用。
环境准备:开启 BAML 调试日志
在动手修改 prompt 前,先开启 BAML 日志,以便后续观察模型的实际推理输出:
export BAML_LOG=debug
BAML_LOG 是 BAML 运行时/CLI 的环境变量,设为 debug 后,所有 LLM 请求的完整 prompt 与响应都会打印到终端。这也是本工作坊在 章节 03 工具循环 中提及的日志开关(该章结束时用 export BAML_LOG=off 关闭)。
提示:如果终端日志过于冗长影响阅读,可以在验证完推理步骤后改用
export BAML_LOG=info或off恢复安静。
修改 Agent Prompt:加入推理步骤
工作坊当前阶段的 Agent 定义位于 baml_src/agent.baml。未修改的 DetermineNextStep 函数 prompt 如下(摘自 baml_src/agent.baml):
{{ _.role("system") }}
You are a helpful assistant that can help with tasks.
{{ _.role("user") }}
You are working on the following thread:
{{ thread }}
What should the next step be?
{{ ctx.output_format }}
它直接询问"下一步是什么?",然后交给 {{ ctx.output_format }} 输出结构化结果。模型的决策过程不可见,只能看到最终的工具调用 JSON。
现在编辑 baml_src/agent.baml,在 {{ ctx.output_format }} 之前插入一段"先规划"的指令。原文档给出的 diff 如下:
baml_src/agent.baml
{{ ctx.output_format }}
+
+ First, always plan out what to do next, for example:
+
+ - ...
+ - ...
+ - ...
+
+ {...} // schema
"#
}
@@assert(b, {{this.a == 3}})
}
-
-
diff 中的 @@assert(...) 行是文件末尾的 BAML 测试断言,保持不变即可(可忽略,属于测试部分,不影响本次修改)。修改完成后的完整函数见仓库提供的标准答案 walkthrough/06-agent.baml:
function DetermineNextStep(
thread: string
) -> HumanTools | CalculatorTools {
client "openai/gpt-4o"
prompt #"
{{ _.role("system") }}
You are a helpful assistant that can help with tasks.
{{ _.role("user") }}
You are working on the following thread:
{{ thread }}
What should the next step be?
{{ ctx.output_format }}
First, always plan out what to do next, for example:
- ...
- ...
- ...
{...} // schema
"#
}
新加入的三段指令含义:
First, always plan out what to do next, for example:—— 强制模型在给出结论前先做规划;- ...占位符 —— 暗示规划应以项目符号列表的形式呈现,模型会自然补充具体步骤(例如 "check current state"、"call multiply tool" 等);{...} // schema—— 提示模型:规划完毕后,最终答案仍必须符合ctx.output_format所要求的结构化 schema。
注意 client "openai/gpt-4o" 引用的 LLM 客户端定义在 baml_src/clients.baml 中,其 options 里 model "gpt-4o"、api_key env.OPENAI_API_KEY,说明推理步骤并不依赖专门推理模型,普通指令模型即可生效。
想跳过手改?直接用现成文件
如果你不想手动编辑,工作坊也提供了跳过步骤:
cp ./walkthrough/06-agent.baml baml_src/agent.baml
walkthrough/06-agent.baml 就是包含推理步骤 prompt 的完整版 Agent 定义,直接复制覆盖即可。
重新生成客户端:让修改生效
BAML 的架构是"定义即代码":baml_src/*.baml 是声明式定义,TypeScript 代码则通过生成器产出。修改 prompt 后必须重新生成客户端,修改才会被编译进调用代码:
npx baml-cli generate
生成行为由 baml_src/generators.baml 控制,关键配置:
output_type "typescript"—— 生成 TypeScript 客户端;output_dir "../"—— 生成代码输出到仓库根目录(生成baml_client/等目录);version "0.85.0"—— BAML 包版本,需与package.json中依赖的baml版本一致;default_client_mode async—— 使b.DetermineNextStep(...)以 async 形式调用。
生成后,src/agent.ts 里 import { AddTool, SubtractTool, DivideTool, MultiplyTool, b } from "../baml_client"(见 src/agent.ts)会自动拿到最新的 prompt 定义。
运行验证:从 BAML 日志观察推理步骤
生成完成后,用一条多步数学指令试运行:
npx tsx src/index.ts 'can you multiply 3 and 4'
命令执行链为:src/index.ts → src/cli.ts 的 cli() → src/agent.ts 的 agentLoop()。CLI 会把命令行参数拼接成用户消息,构造初始线程(见 src/cli.ts):
const thread = new Thread([{ type: "user_input", data: message }]);
const result = await agentLoop(thread);
agentLoop 的核心循环(见 src/agent.ts):
export async function agentLoop(thread: Thread): Promise<Thread> {
while (true) {
const nextStep = await b.DetermineNextStep(thread.serializeForLLM());
console.log("nextStep", nextStep);
thread.events.push({ "type": "tool_call", "data": nextStep });
switch (nextStep.intent) {
case "done_for_now":
case "request_more_information":
return thread;
case "add":
case "subtract":
case "multiply":
case "divide":
thread = await handleNextStep(nextStep, thread);
}
}
}
由于此前已 export BAML_LOG=debug,运行时会看到 BAML 打印的完整日志,其中包含:
- 发给
gpt-4o的完整 prompt(含你新加的推理步骤指令); - 模型返回的推理/规划文本(在最终结构化输出之前的那段项目符号列表);
- 最终解析出的结构化
nextStep(如{ "intent": "multiply", "a": 3, "b": 4 })。
这正是"推理步骤"带来的核心收益:决策过程不再是个黑盒,你能看到模型为什么选择 multiply,从而更容易发现 prompt 缺陷并迭代。
可选挑战:把推理步骤写进结构化输出
原文档留下了一个进阶练习:给工具输出格式增加一个字段,把推理步骤一并放进结构化输出。
这需要两步:
- 在 baml_src/tool_calculator.baml 中,为工具类增加一个
reasoning字段,例如:
class MultiplyTool {
intent "multiply"
a int | float
b int | float
reasoning string @description("short explanation of why this step was chosen")
}
- 在 prompt 中要求模型把推理内容填充到该字段,例如在推理步骤指令中补充
"reasoning": "..."示例。
完成后再次 npx baml-cli generate,nextStep 的 JSON 里就会出现推理说明,Agent 的每一步决策都可以被持久化、审计或展示给用户。这与 Factor 2 的"Transparency(透明性)"价值直接呼应——"确切知道你给 Agent 的指令是什么",现在更进一步:"确切知道 Agent 为什么这么做"。
测试佐证:BAML 断言如何兜底 prompt 修改
修改 prompt 属于高风险操作,可能影响工具选择行为。仓库通过 baml_src/agent.baml 中的一组 BAML 测试来保证行为不回退(walkthrough/06-agent.baml 中同样保留):
MathOperation:输入"can you multiply 3 and 4?",断言intent == "multiply";LongMath:多步运算(乘 → 除 → 加),断言最终intent == "done_for_now"且消息中包含"18";MathOperationWithClarification:非法输入"feee9ff10",断言应请求澄清;MathOperationPostClarification:澄清后("lets try 12 instead")应回到multiply且b == 12。
这些用例说明:prompt 是可以通过测试来守护的代码资产——修改推理步骤后,运行这些断言即可快速确认工具选择与澄清流程没有被破坏,这正是 Factor 2 中 "Testing and Evals" 价值的落地实践。
小结
本章通过一个最小但完整的 Agent(BAML 定义 + TypeScript 工具循环)演示了"拥有你的 prompt"的实操路径:
- 开启日志(
export BAML_LOG=debug)获得可观测性; - 在
agent.baml中注入推理步骤,把"先规划再行动"写进 prompt; npx baml-cli generate重新生成客户端,让定义变更生效;- 运行 CLI 验证,从日志中观察模型推理文本与最终结构化工具调用;
- 可选进阶:将推理步骤加入工具输出结构,让决策过程可持久化、可审计;
- 测试兜底:利用 BAML 断言守护 prompt 修改,防止工具选择与澄清流程回退。
掌握这套方法后,你可以把同样的模式迁移到任何 BAML 驱动的 Agent 上——无论是计算器工具,还是 factor-02-own-your-prompts 示例中的部署 Agent(ListGitTags、DeployBackend、DeployFrontend 等),prompt 始终是你与应用逻辑、LLM 之间唯一且完全可控的接口。