AI Agents in Action · 第十一章

回到第一章的
那五层

最后一章没有新东西——它回到第一章:按那五个功能层, 每层给一段「最佳实践」代码,再加三个场景的配方。是全书的收口。

但它也是全书代码质量最差的一章。8 个文件加起来才 309 行, 我逐个跑过:2 个直接 TypeError 崩溃,1 个是第七章的复制品, 1 个有永远执行不到的死代码。所以这一章的正确读法不是照抄, 是把它当一份「提醒清单」,代码只看思路。

1
人设 · Persona
01_core_persona_tips.py
能跑
2
工具与动作
02_tool_action_tips.py
有死代码
3
推理与规划
03_reasoning_planning_tips.py
能跑
4
知识与记忆
04_knowledge_memory_tips.py
能跑
5
评估与反馈
05_evaluation_feedback_tips.py
第七章复制品
场景
客服智能体
06_support_agent_tips.py
TypeError
场景
RAG 智能体
07_rag_agent_tips.py
只定义不运行
场景
深度研究智能体
08_deep_research_agent_tips.py
TypeError

右列是我逐个真跑一遍的结果,不是看出来的。

动手之前

这一章的代码是「示意」,不是「示例」

前十章的代码基本都能跑(除了我指出的那些坑)。这一章不行—— 它更像是书里排版用的片段,好几个文件里直接写着字面的省略号:

retrieval_agent = Agent(...)  # add retrieval agent here
web_search_tool = ...           # WebSearchTool() or your own @function_tool

Python 里 ... 是个合法的值Ellipsis), 所以语法检查能过——但一跑就炸。这也解释了为什么它们能混进仓库: 没人跑过,只做了语法检查(和第十章那两个大文件一个毛病)。

所以别指望 python 06_support_agent_tips.py 能出东西。 把这一章当成「检查清单」读:看每段代码在提醒你什么, 然后回头检查自己第 2–10 章写的东西有没有漏。

全章零 load_dotenv

和第三、四、五、九章一样,chapter_11/没有一个文件调 load_dotenv()。下面的代码块我都补上了配置。

第 1 层

人设:把「今天几号」注进去

这一段是全章最有价值的一个技巧,而且只有一行的差别。

动态 instructions:人设不是常量

对应 11.1.1 · 01_core_persona_tips.py · 能跑 ✓

from datetime import date

from agents import Agent, Runner, set_default_openai_api, set_tracing_disabled
from dotenv import load_dotenv
from pydantic import BaseModel, Field

# 仓库原文没有这三行,是为了接第三方模型加的
load_dotenv()
set_default_openai_api("chat_completions")
set_tracing_disabled(True)


class Answer(BaseModel):
    """机器可读的回答格式。"""

    answer: str = Field(..., description="简洁的、给用户看的回答")
    citations: list[str] = Field(default_factory=list)
    confidence: float = Field(..., ge=0, le=1)


def core_instructions(ctx, agent) -> str:
    # ↓ 每次运行时才算,所以「今天」永远是对的
    today = date.today().isoformat()
    return (
        f"你是 SupportMentor,一个乐于助人的领域助手。今天是 {today}。\n"
        "始终要:\n"
        "1) 简洁作答;2) 优先使用检索到的上下文;3) 上下文里没有就说你不知道。\n"
        "输出必须是符合 Answer schema 的合法 JSON。\n"
        "绝不要:破例修改政策,或者编造事实。"
    )


core_agent = Agent(
    name="SupportMentor", instructions=core_instructions, output_type=Answer
)

input = "我们配件的退款窗口期是多久?"
result = Runner.run_sync(core_agent, input)
print(result.final_output)
instructions 传函数,而不是字符串

instructions=core_instructions——传的是函数本身,不是调用它。 SDK 会在每次运行时调用它,现算出一段人设。

为什么重要?因为字符串是在 import 时就定死的。你要是写 instructions=f"今天是 {date.today()}", 这个「今天」就永远停在你启动服务那一天—— 服务跑一个月,它还以为是一个月前。这是个真实的线上事故来源。

第五章 Reflexion 那节用过同一个机制(把上一轮的批评拼进人设), 这里给出了它更日常的用途:任何随时间变化的东西,都必须动态注入。

这段提示词的三段式结构值得抄

身份 → 始终要(编号列表)→ 绝不要。「绝不要」单独成段, 而且只放最关键的两条(不破例、不编造)。

对比第二章那个随手写的「你是一个研究规划助手」——这才是生产级人设该有的样子。 另外注意 confidence: float = Field(..., ge=0, le=1)ge/le 约束:不光要模型给个数,还要在 schema 层面卡死范围

第 2 层

工具:工具报错了怎么办

failure_error_function:别让工具的异常炸掉整个 agent

对应 11.1.2 · 02_tool_action_tips.py

import json

from agents import (Agent, ModelSettings, Runner, function_tool,
                    set_default_openai_api, set_tracing_disabled)
from dotenv import load_dotenv

# 仓库原文没有这三行,是为了接第三方模型加的
load_dotenv()
set_default_openai_api("chat_completions")
set_tracing_disabled(True)


def _failure_error_function(context, e) -> str:
    # 把异常包成一段 JSON,交回给模型看,而不是让它炸穿
    return json.dumps({"status": "error", "message": str(e)})


@function_tool(failure_error_function=_failure_error_function)
def lookup_order(order_id: str) -> dict:
    """按订单号查询状态。用户问自己订单时使用。"""
    if not order_id.startswith("ORD-"):
        raise ValueError("订单号格式不对")
    return {"status": "shipped", "eta_days": 3}
    # 仓库原文这里有第二条一模一样的 return,是死代码,已删


tooling_agent = Agent(
    name="ToolingAgent",
    instructions="需要时使用工具;用 JSON 回复。",
    tools=[lookup_order],
    model_settings=ModelSettings(
        tool_choice="auto",           # 让模型自己决定要不要调工具
        parallel_tool_calls=True,     # 允许一次并发调多个工具
    ),
)
print(
    Runner.run_sync(tooling_agent, "订单 ORD-123 现在什么状态?").final_output
)
这是全章第二有用的技巧

回想第二章 08 那个 bug:search_sources[research_soruce] 直接下标,模型传个没有的键就 KeyError整个 agent 当场崩

failure_error_function 就是解药:工具抛异常时, 不往上炸,而是把错误信息包成一段文本交回给模型。 模型看到「订单号格式不对」,就能自己反应过来—— 「哦,我该问用户要正确的订单号」,而不是整个流程死掉。

这把「程序崩溃」变成了「对话的一部分」。 对工具多、链路长的 agent(第四章往后全是),这几乎是必需的。

仓库这里有两条一模一样的 return

原文第 16、17 行是两条完全相同的 return—— 第二条永远执行不到,是复制粘贴残留的死代码。上面这版我删了。 无害,但它是这一章「没人跑过、也没人读过」的又一个佐证。

第 3 / 4 / 5 层

剩下三层:老熟人

这三个文件基本是前面章节的浓缩版,没有新东西,我只点出各自的落点。

第 3 层 · 推理与规划

03_reasoning_planning_tips.py(61 行)· 能跑 ✓

sequential-thinking MCP server + output_type—— 就是第五章的浓缩。落点是:推理的产出也要结构化, 别让模型想完之后吐一段散文。

第 4 层 · 知识与记忆

04_knowledge_memory_tips.py(61 行)· 能跑 ✓

两个工具:retrieve()remember()—— 第六章那两种脑子的最小骨架。落点是:「查」和「记」是两个动作, 要给模型两把独立的工具,而不是混成一个。

第 5 层 · 评估与反馈

05_evaluation_feedback_tips.py(31 行)

这个文件就是第七章那个,一字不差

我 diff 过:chapter_11/05_evaluation_feedback_tips.pychapter_07/09_arize_phoenix_tracing.py 只差第一行的 docker 启动注释,其余完全相同。

所以这一层的「建议」其实就是:接上 Phoenix。 它用的正是我们在第七章验证过的那一行 set_trace_processors([])—— 对用第三方模型的你,这仍然是唯一能看到追踪的路径。 代码和说明见第七章,这里不重复了。

三个场景配方

06 / 07 / 08:思路好,代码崩

最后三个文件给的是三种常见 agent 的配方。两个直接 TypeError—— 但它们提醒的事仍然值钱。

客服智能体:最重要的工具是「转人工」

对应 11.2 · 06_support_agent_tips.py · TypeError ✗

from agents import Agent, ModelSettings, function_tool


@function_tool
def escalate_to_human(ticket_id: str, reason: str) -> str:
    """把这次对话转给人工。用于愤怒的用户、大额退款,或政策不明确的情况。"""
    # 在你自己的系统里建单/路由……
    return f"已转人工,工单 {ticket_id}:{reason}"


retrieval_agent = Agent(...)   # ← 字面省略号,跑到这里就 TypeError

support = Agent(
    name="Triage Support Agent",
    instructions=(
        "涉及账户操作前先核实身份。引用政策条款。"
        "如果不确定,或者用户情绪激动,调用 escalate_to_human。"
        "复杂的查询转给 retrieval_agent。"
    ),
    tools=[
        escalate_to_human,
        retrieval_agent.as_tool(),   # ← 这里也错:as_tool() 少了两个必填参数
    ],
    model_settings=ModelSettings(tool_choice="auto"),
)
我跑了,两处都会炸

$ python 06_support_agent_tips.py
TypeError: Agent name must be a string, got ellipsis

第一处Agent(...) 里那个省略号是真的省略号对象, SDK 拿它当 name,当场报错。

第二处(更值得注意)retrieval_agent.as_tool() 是错的 API 用法,不只是占位问题。我查了签名—— as_tool()两个必填参数

as_tool(tool_name, tool_description, ...)  # 两个都必填

书里三处 as_tool() 调用全是空的(06 一处、08 两处)。 就算你把省略号补上,这行照样炸。正确写法要给工具起名字和描述—— 这其实很合理:模型是靠名字和描述决定调不调它的(第二章讲过)。

但这个思路是这一章最值钱的东西之一

escalate_to_human——把「转人工」做成一个工具。

整本书讲了十章怎么让 agent 更能干,而客服场景最重要的能力是「知道自己不该处理」。 它和第十章的「知道自己不知道」是同一个思想,但更实用: 第十章是让 agent 说「我不确定」,这里是让它把事交出去给人

注意 docstring 写得很具体:「愤怒的用户、大额退款、政策不明确」—— 不是「必要时转人工」这种废话。第二章说过 docstring 是给模型看的提示词, 这里是个正面范例。

RAG 智能体:20 行,全部重点在一句提示词

对应 11.3 · 07_rag_agent_tips.py · 只定义不运行

from agents import Agent, function_tool


@function_tool
def retrieve(query: str, corpus: str = "product_docs", top_k: int = 5) -> list[dict]:
    """从指定的索引里返回 top-k 段落及其元数据。"""
    # ... 这里接你的向量检索 + 元数据过滤 ...
    return [{"text": "...", "source": "KB-123", "section": "Refunds"}]


answerer = Agent(
    name="RAG-Answerer",
    instructions=(
        "只使用通过 `retrieve` 拿到的段落。"
        "如果它们都回答不了这个问题,就回复:"
        "'根据现有文档我无法回答。' "
        "给出简短的总结,并标注来源。"
    ),
    tools=[retrieve],
)
「只使用」和「回答不了就说不知道」

这 20 行里真正的内容就两句提示词,但它们正是第七章 grounding 那一整节的浓缩

只使用通过 retrieve 拿到的段落」——防夹带私货。
「回答不了就说『根据现有文档我无法回答』」——给模型一条合法的退路

第二句比第一句重要。你不给它一个「说不知道」的标准答法, 它就会编——因为模型的默认行为是无论如何都要给个回答「不知道」必须被明确授权,它才敢说。

注意返回的字典带 sourcesection—— 元数据是为了让模型能标注来源。没有它,「标注来源」这条指令就是空的。

深度研究智能体:作者的三角配方

对应 11.4 · 08_deep_research_agent_tips.py · TypeError ✗

这个文件把第九章的东西压成了一个三角结构: 研究者(planner)+ 批评者(critic)+ 写手(writer), 后两个作为工具挂给研究者。

from agents import Agent

web_search_tool = ...   # ← 字面省略号:WebSearchTool() 或你自己的 @function_tool
extract_tool = ...      # ← 例如网页提取器/清洗器
analyze_tool = ...      # ← 例如做统计的代码解释器

critic = Agent(name="Critic", instructions="检查完整性、偏见、自相矛盾之处。")
writer = Agent(
    name="Writer", instructions="综合成一份带引用的简明报告。"
)

researcher = Agent(
    name="ResearchPlanner",
    instructions=(
        "拆解研究目标。事实必须通过网页/文档工具获取。"
        "追踪来源,避免无依据的断言。"
    ),
    tools=[
        web_search_tool,
        critic.as_tool(),    # ← 同样少了 tool_name / tool_description
        writer.as_tool(),
    ],
)
critic 和 writer 分开,是对的

这正好呼应第九章那个发现:循环里的每一轮只看得见局部, 所以「合成」必须单独一步。这里把 writer 单列出来, 就是这个道理。

critic(检查完整性、偏见、矛盾)是第七章 LLM-as-judge 的应用—— 让一个智能体评判另一个的产出

as_tool() 而不是 handoff,也是有讲究的: 第四章说过,移交是接力棒(转出去不回来), agent 当工具是总控调完还在自己手上。 研究者需要「叫批评者看一眼,然后自己接着改」——所以必须用工具,不能用移交

全书收口

这一章其实是一份检查清单

把它当清单用,别当代码用

剥掉那些跑不了的代码,这一章真正给你的是八个提醒。 我按「值不值得回头改自己的代码」排了个序:

① 人设里凡是会变的东西,必须动态注入。 instructions= 传函数不传字符串。这是唯一一个 不改就会出线上事故的。

② 工具要有 failure_error_function 让工具的异常变成对话的一部分,而不是崩溃。 (回头看第二章 08 那个 KeyError。)

③ 给模型一条「说不知道」的标准答法。 不明确授权,它就编。

④ 把「转人工」做成工具。 知道自己不该处理,比什么都能处理更有价值。

⑤ 检索结果要带元数据。不然「标注来源」是空话。

⑥ 「查」和「记」是两个工具。别混。

⑦ 推理的产出也要 output_type (第五章 ToT 那个 if "promising" in ... 的教训。)

⑧ 接 Phoenix。——对你来说这是唯一能看到追踪的方式。

一条贯穿全书的线,在这里收口了

第一章说 agent 有五个功能层,第十一章按这五层给建议——书的结构闭合了

但更有意思的是另一条线。回头数一数这本书反复栽的坑: 第四章拿 in 当护栏、第五章拿 in 筛分支、 第七章拿 == 判对错、第十章拿词交集判语义—— 四次都是同一个病:用字符串操作去理解语言。

而解药第二章就教了,就是 output_type。 它在第二章只是「方便解析」,第七章成了「让判断能被代码消费」, 第九章成了「模型控制程序流的方向盘」,第十章成了「元认知的前提」。 如果这本书只有一个东西值得记住,是它。

跟着书走之前,先看这几条