我按官方文档操作了3遍,每次都卡在同一个地方——Agents SDK 2.0 真实踩坑记录
我按官方文档操作了3遍,每次都卡在同一个地方——Agents SDK 2.0 真实踩坑记录
"这文档写得很清楚啊,跟着走不就行了?"
我当时也是这么想的。
然后我花了整整一个下午,在同一个报错上绕了三圈。Quick Start 的代码我复制粘贴了,API Key 我配置了,依赖我装了——但就是跑不起来。报错信息看起来和我的代码毫无关系,Google 搜不到,GitHub Issues 里有几个类似的帖子但没有人给出解决方案。
如果你也有过这种体验,这篇文章就是为你写的。
我不打算重复官方文档已经写清楚的内容。我要把两个文档没有明说、但会让你卡死的暗坑挖出来,摆在你面前——带着真实的报错信息、错误写法和修复方案,以及一个从头到尾能跑通的多步骤 Agent 案例。
---
一、在开始之前:把环境配对
很多人踩坑,其实从这一步就开始了,只是要到后面才会爆。
环境版本清单
在开始之前,请确认你的环境和下面这张表一致:
| 依赖项 | 推荐版本 | 备注 | | Python | 3.10+ | 低于 3.10 会有类型注解兼容问题 | |openai-agents | 0.0.12+ | SDK 2.0 对应版本 |
| openai | 1.30+ | 作为底层 API 客户端 |
| pydantic | 2.x | SDK 内部强依赖,版本不对会静默出错 |
安装顺序也有讲究,不要直接 pip install openai-agents 了事:
# 推荐用虚拟环境隔离
python -m venv agents-env
source agents-env/bin/activate # Windows 用: agents-env\Scripts\activate
按顺序安装,避免依赖冲突
pip install "openai>=1.30.0"
pip install "pydantic>=2.0.0"
pip install "openai-agents>=0.0.12"
API Key 的正确配置方式
这里有一个细节,文档一笔带过了:SDK 在初始化时会同时读取 OPENAI_API_KEY 和 OPENAI_BASE_URL 两个环境变量。如果你用的是第三方兼容接口(比如后面我会提到的 api.884819.xyz),只设置 Key 是不够的,Base URL 也要一起配。
# 不够,只有这个会导致请求打到 OpenAI 官方而非你的接口
export OPENAI_API_KEY="your-key-here"
正确做法:两个都要配
export OPENAI_API_KEY="your-key-here"
export OPENAI_BASE_URL="https://api.884819.xyz/v1"
或者在代码里显式传入(更推荐,不容易被环境变量污染):
import os
from openai import AsyncOpenAI
from agents import set_default_openai_client
client = AsyncOpenAI(
api_key="your-key-here",
base_url="https://api.884819.xyz/v1"
)
set_default_openai_client(client)
好,环境稳了。现在来搭真正的东西。
---
二、主体实战:搭一个"搜索+摘要+报告"三步 Agent
我选这个场景是因为它足够真实:给定一个主题,Agent 先搜索相关信息,再对搜索结果做摘要,最后生成一份结构化报告。三个步骤,三次 Tool 调用,上下文需要在步骤间传递。
这正是会把两个坑都踩到的最小可复现场景。
执行流程
用户输入主题
│
▼
[Step 1] search_web(topic)
│ 返回:搜索结果列表
▼
[Step 2] summarize_content(raw_results)
│ 返回:摘要文本
▼
[Step 3] generate_report(topic, summary)
│ 返回:最终报告
▼
输出给用户
Agent Loop 在每一步结束后,会把 Tool 的返回值追加到上下文,然后决定是否继续调用下一个 Tool,还是直接输出最终答案。
完整代码
"""
三步 Agent:搜索 → 摘要 → 报告
运行环境:Python 3.10+, openai-agents 0.0.12+
"""
import asyncio
import json
from typing import Any
from openai import AsyncOpenAI
from agents import Agent, Runner, function_tool, set_default_openai_client
── 1. 初始化客户端 ──────────────────────────────────────────────────────────
client = AsyncOpenAI(
api_key="your-key-here", # 替换为你的 Key
base_url="https://api.884819.xyz/v1" # 支持多模型,国内直连
)
set_default_openai_client(client)
── 2. 定义三个 Tool ──────────────────────────────────────────────────────────
@function_tool
def search_web(topic: str) -> str:
"""
模拟搜索工具:根据主题返回相关信息。
注意:返回类型必须是 str,不能是 dict(坑1的所在,见第四章)
"""
# 实际场景中这里接入真实搜索 API,比如 Tavily 或 SerpAPI
# 这里用模拟数据演示结构
results = [
{"title": f"关于{topic}的最新进展", "snippet": f"{topic}领域在近期出现了多项突破性进展..."},
{"title": f"{topic}核心技术解析", "snippet": f"从技术角度分析,{topic}的核心在于..."},
{"title": f"{topic}的应用场景", "snippet": f"目前{topic}已被广泛应用于..."},
]
# ⚠️ 关键:用 json.dumps 序列化为字符串,而不是直接返回 list
return json.dumps(results, ensure_ascii=False)
@function_tool
def summarize_content(raw_results: str) -> str:
"""
摘要工具:对搜索结果做结构化摘要。
接收 JSON 字符串,返回摘要文本字符串。
"""
try:
results = json.loads(raw_results)
snippets = [r.get("snippet", "") for r in results]
combined = " ".join(snippets)
# 实际场景可以在这里再调一次 LLM 做摘要
# 这里直接拼接演示
summary = f"综合摘要(共{len(results)}条):{combined[:300]}..."
return summary
except json.JSONDecodeError:
return f"摘要失败:输入格式不正确,原始内容:{raw_results[:100]}"
@function_tool
def generate_report(topic: str, summary: str) -> str:
"""
报告生成工具:根据主题和摘要生成结构化报告。
"""
report = f"""
{topic} 研究报告
执行摘要
{summary}
核心发现
基于以上摘要,{topic}领域的关键趋势包括:
1. 技术层面持续演进
2. 应用场景不断扩展
3. 行业关注度持续上升
结论
{topic}值得持续关注,建议定期跟踪最新进展。
报告生成时间:自动生成
""".strip()
return report
── 3. 配置 Agent ─────────────────────────────────────────────────────────────
research_agent = Agent(
name="ResearchAgent",
model="gpt-4o", # 或换成 deepseek-chat、qwen-max 等兼容模型
instructions="""
你是一个研究助手,负责对给定主题进行系统性研究。
请严格按照以下步骤执行,不要跳过任何步骤:
1. 首先调用 search_web 搜索主题相关信息
2. 将搜索结果传给 summarize_content 生成摘要
3. 最后调用 generate_report 生成完整报告
4. 将报告内容作为你的最终回复输出给用户
重要:每一步的输出都要完整传递给下一步,不要截断。
""",
tools=[search_web, summarize_content, generate_report],
)
── 4. 运行 Agent ─────────────────────────────────────────────────────────────
async def run_research(topic: str) -> None:
print(f"\n🔍 开始研究主题:{topic}\n")
print("=" * 50)
result = await Runner.run(
research_agent,
input=f"请对以下主题进行完整研究并生成报告:{topic}",
max_turns=10, # ⚠️ 这个参数和坑2直接相关,见第四章
)
print("\n📄 最终报告:")
print(result.final_output)
print("\n✅ 研究完成")
if __name__ == "__main__":
asyncio.run(run_research("大语言模型的推理优化技术"))
💡 注意:文中所有代码示例使用的是兼容 OpenAI 格式的 API 接口。如果你还没有可用的 API Key,推荐直接用 [api.884819.xyz](https://api.884819.xyz)——支持 GPT、Claude、Deepseek 等多模型切换,国内直连无需代理,新用户注册即送体验 token,本文所有代码在这个接口上测试通过,直接替换 base_url 即可跑起来。
---
三、我踩的 2 个文档没写清楚的坑
好,基础代码你有了。现在是这篇文章最值钱的部分。
坑 1:Tool 返回值的隐性类型约束
错误现象你的 Tool 函数运行正常,返回了一个 dict 或 list,但 Agent 在处理这个 Tool 的返回值时抛出类似这样的错误:
TypeError: Object of type list is not JSON serializable
或者
ValidationError: 1 validation error for FunctionCallOutput
value
str type expected (type=type_error.str)
根因分析
SDK 在处理 @function_tool 的返回值时,内部会把返回值当作字符串追加进对话历史。如果你返回的是 dict 或 list,SDK 会尝试隐式转换,但转换逻辑在某些嵌套结构下会失败,而且报错指向的是 SDK 内部的序列化层,完全看不出是你的 Tool 返回值有问题。
# ❌ 错误写法:直接返回 dict 或 list
@function_tool
def search_web(topic: str) -> dict:
results = [{"title": "...", "snippet": "..."}]
return {"results": results, "count": len(results)} # 这里会埋雷
✅ 正确写法:始终返回 str,复杂对象用 json.dumps 序列化
@function_tool
def search_web(topic: str) -> str:
results = [{"title": "...", "snippet": "..."}]
return json.dumps({"results": results, "count": len(results)}, ensure_ascii=False)
修复规则很简单:@function_tool 装饰的函数,返回类型签名和实际返回值都必须是 str。如果你需要传递结构化数据,序列化成 JSON 字符串,在下一个 Tool 里再反序列化。多一行代码,省掉半天排查时间。
---
坑 2:多步骤中的上下文静默截断
这个坑比第一个危险得多,因为它不报错。
错误现象你的三步 Agent 在前两步运行正常,但到第三步,Agent 的输出开始变得奇怪——它似乎"忘记"了第一步搜索到的内容,生成的报告和主题完全对不上,或者开始胡乱填充内容。
你检查代码,逻辑没问题。你检查 Tool,返回值也正确。但结果就是越来越离谱。
根因分析Runner.run() 有一个 max_turns 参数,默认值在不同版本中有差异(某些版本默认是 10)。但更关键的是:当 Agent Loop 运行多步之后,累积的对话历史(包括所有 Tool 调用和返回值)可能超过模型的上下文窗口,SDK 会静默地截断早期历史,优先保留最近几轮的内容。
截断发生时,不会有任何警告或异常。Agent 只是"自然地"用更少的信息继续工作,结果当然越来越差。
# 实际发生的事情(SDK 内部行为,不对外暴露):
Turn 1: [system] + [user: 研究主题X]
Turn 2: [system] + [user] + [tool_call: search_web] + [tool_result: 大量搜索结果]
Turn 3: [system] + [user] + [tool_call] + [tool_result] + [tool_call: summarize] + [tool_result: 摘要]
Turn 4: 上下文接近窗口上限 → SDK 开始截断早期内容
[system] + ... + [tool_call: generate_report]
↑ 此时 topic 参数可能已从上下文中消失
错误写法 vs 正确写法
# ❌ 错误写法:完全依赖 SDK 自动管理上下文
result = await Runner.run(
research_agent,
input=f"请研究:{topic}",
# 没有任何上下文管理策略
)
✅ 正确写法:在关键 Tool 里显式传递核心信息,不依赖历史上下文
@function_tool
def generate_report(topic: str, summary: str) -> str:
"""
注意:topic 和 summary 都作为参数显式传入
而不是让 Agent 从历史上下文里"回忆"
这样即使上下文被截断,关键信息也不会丢失
"""
# ...
✅ 同时,在 instructions 里明确要求 Agent 显式传参
instructions = """
...
重要:调用 generate_report 时,必须将原始主题和摘要文本作为参数显式传入,
不要依赖你对之前步骤的"记忆"。
"""
顺带说一句,如果你在测试过程中遇到 API 响应不稳定,导致难以判断是代码问题还是接口问题,换一个稳定的接口环境会让排查效率高很多——这也是我后来统一用 [api.884819.xyz](https://api.884819.xyz) 做测试的原因。响应稳定,排查变量少,问题更容易定位。
---
四、跑通之后——让 Agent 更稳定的 3 个实用建议
踩坑之后,你比大多数人更懂这个 SDK。但"能跑通"和"稳定可用"之间还有一段距离。
建议 1:给 Tool 返回值加防御性校验
不要信任 Tool 的输入,哪怕是 Agent 传过来的。
@function_tool
def summarize_content(raw_results: str) -> str:
"""带防御性校验的摘要 Tool"""
# 防御性检查
if not raw_results or not raw_results.strip():
return "错误:输入内容为空,无法生成摘要"
try:
data = json.loads(raw_results)
if not isinstance(data, (list, dict)):
return f"错误:期望 JSON 数组或对象,收到:{type(data).__name__}"
# ... 正常处理逻辑
except json.JSONDecodeError as e:
return f"错误:JSON 解析失败 - {str(e)},原始内容前100字:{raw_results[:100]}"
原则:Tool 永远不要抛出未捕获的异常,因为异常会中断整个 Agent Loop,而且报错信息往往指向 SDK 内部,极难定位。用防御性返回值代替异常,让 Agent 有机会自我纠正。
建议 2:手动管理上下文,不依赖 SDK 自动处理
对于超过 3 步的 Agent,核心信息应该显式传递,而不是依赖历史上下文。
具体做法:- 在每个关键 Tool 的参数列表里,显式包含它需要的所有信息
- 在
instructions里要求 Agent 显式传参,而不是"记住"之前的结果 - 对于特别长的中间结果,考虑在 Tool 内部做截断,只保留关键信息传递给下一步
建议 3:本地调试时用 stream 模式观察中间步骤
不要等到最终结果出来才发现哪里不对。
# 使用 stream 模式,实时观察 Agent 的每一步决策
async def run_with_debug(topic: str) -> None:
async with Runner.run_streamed(
research_agent,
input=f"请研究:{topic}",
max_turns=10,
) as stream:
async for event in stream:
# 打印每一个事件,包括 Tool 调用和返回值
print(f"[Event] {event.type}: {str(event)[:200]}")
result = await stream.get_final_result()
print(f"\n最终输出:{result.final_output}")
Stream 模式下,你能实时看到 Agent 在每一步做了什么决定、调用了哪个 Tool、Tool 返回了什么——这是排查"结果越来越离谱"问题的最有效手段。
---
最后
这篇文章解决的是"单 Agent 跑通"的问题。
但你可能已经发现了:当任务更复杂,一个 Agent 根本不够用。搜索、摘要、报告,如果每个环节都需要专门的能力和上下文,单 Agent 的 instructions 会越写越长,越来越难以维护,而且上下文截断的问题会更严重。
下一篇我会写:用 Agents SDK 2.0 搭 Multi-Agent 协作系统——多个 Agent 怎么分工、怎么传递任务、主 Agent 怎么调度子 Agent。 这才是 SDK 2.0 真正让人兴奋的地方,也是坑最多的地方。Orchestrator-Worker 模式、Agent 间的状态共享、子 Agent 失败时的降级策略——这些在官方文档里几乎没有实战级别的说明。关注我,下篇更新时第一时间收到通知。
---
本文由8848AI原创,转载请注明出处。关注8848AI,带你从零开始学AI。 新用户注册即送体验token,国产模型(Deepseek/千问等)完全免费,没有月租,按量付费。立即体验:[api.884819.xyz](https://api.884819.xyz)#AI教程 #AgentsSDK #Python #LLM应用开发 #8848AI #Agent实战 #人工智能开发 #踩坑记录