12-factor-agents 实战:用推理步骤自定义 Agent Prompt,让工具调用更可靠

原创2026-09-30 15:44:141,449 阅读
文章标签:文档教程人工智能大模型AI Agent

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 源文件里,逐字可控、可测试、可演进。

own-your-prompts 架构图 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 打印的完整日志,其中包含:

  1. 发给 gpt-4o 的完整 prompt(含你新加的推理步骤指令);
  2. 模型返回的推理/规划文本(在最终结构化输出之前的那段项目符号列表);
  3. 最终解析出的结构化 nextStep(如 { "intent": "multiply", "a": 3, "b": 4 })。

这正是"推理步骤"带来的核心收益:决策过程不再是个黑盒,你能看到模型为什么选择 multiply,从而更容易发现 prompt 缺陷并迭代。

可选挑战:把推理步骤写进结构化输出

原文档留下了一个进阶练习:给工具输出格式增加一个字段,把推理步骤一并放进结构化输出。

这需要两步:

  1. 在 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")
}
  1. 在 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"的实操路径:

  1. 开启日志(export BAML_LOG=debug)获得可观测性;
  2. 在 agent.baml 中注入推理步骤,把"先规划再行动"写进 prompt;
  3. npx baml-cli generate 重新生成客户端,让定义变更生效;
  4. 运行 CLI 验证,从日志中观察模型推理文本与最终结构化工具调用;
  5. 可选进阶:将推理步骤加入工具输出结构,让决策过程可持久化、可审计;
  6. 测试兜底:利用 BAML 断言守护 prompt 修改,防止工具选择与澄清流程回退。

掌握这套方法后,你可以把同样的模式迁移到任何 BAML 驱动的 Agent 上——无论是计算器工具,还是 factor-02-own-your-prompts 示例中的部署 Agent(ListGitTags、DeployBackend、DeployFrontend 等),prompt 始终是你与应用逻辑、LLM 之间唯一且完全可控的接口。

登录后查看全文
12-factor-agents