
1. 从零理解 OpenAI Agents SDK 到底在解决什么问题第一次看到“OpenAI Agents SDK 构建指南”这个标题很多人脑子里冒出来的第一个念头大概是这不就是又一个调 API 的封装库吗我一开始也这么想直到真正把一个多轮工具调用的需求塞进传统的 Chat Completions 流程里被那一堆手写的 function call 解析、状态维护、循环判断折磨了整整两天之后才回过头来认真研究这套 SDK 的设计意图。它要解决的核心问题其实非常具体让模型从“只会聊天”变成“能自己决定调用什么工具、按什么顺序调用、拿到结果后继续推理”的智能体而开发者不用再自己写那个又臭又长的 while 循环。传统做法里你要实现一个能查天气、能查数据库、能根据结果再决定下一步的助手代码大概长这样先发一条带 tools 定义的请求拿到返回后判断 finish_reason 是不是 tool_calls是的话解析出函数名和参数本地执行函数把结果作为 role 为 tool 的消息再塞回对话历史然后再发一次请求如此往复。这个循环本身不难难的是当工具有十几个、调用可能嵌套、中间还要处理异常和超时的时候整个状态机就会变得极其脆弱。OpenAI Agents SDK 把这套循环抽象成了Agent智能体 Runner运行器 Tools工具 Handoffs交接四个概念你只需要声明“这个智能体能用什么工具”剩下的编排交给 Runner。这套东西适合谁如果你只是做一个单轮问答的客服机器人坦白说用不上直接调 Chat Completions 更省事。但只要你的场景里出现了“根据用户问题决定查哪个数据源”“多个步骤需要串联”“不同子任务交给不同角色处理”这类需求Agents SDK 就能帮你省掉大量胶水代码。它尤其适合做基于向量数据库与对话引擎的智能知识库这类项目——因为知识库问答天然就是“先检索、再判断、必要时追问、最后组织答案”的多步流程正好是 Agent 的用武之地。我写这个系列的第一篇不打算一上来就堆 API 文档而是想先把“为什么这么设计”讲透。因为只有理解了 Runner 内部那个循环在干什么你后面调参、排查问题、设计工具时才会有方向感而不是照着示例抄一遍换个场景就抓瞎。2. 核心概念拆解与设计思路2.1 Agent 不是模型而是一份“岗位说明书”很多人第一次接触会把 Agent 理解成“一个更聪明的模型”这其实是误解。在 SDK 里Agent 是一个配置对象它回答的是几个问题用哪个模型、给它什么指令instructions、它能用哪些工具、遇到搞不定的事情可以移交给谁。你可以把它类比成公司里的一份岗位说明书——这个人模型本身的能力是固定的但你在说明书里写清楚了他的职责边界和可用资源。这样设计的好处是职责分离。模型能力升级了你换 model 参数就行业务逻辑变了你改 instructions 和 tools 列表就行两者互不干扰。我见过不少项目把提示词和业务判断硬编码在一起改一个需求要动好几处最后没人敢碰。Agent 这种声明式写法本质上是在逼你把“智能体是什么”和“智能体怎么跑”分开。一个最小的 Agent 定义大概包含这几个字段name名字用于日志和交接识别、instructions系统提示词、model模型名、tools工具列表、handoffs可交接的目标 Agent 列表。注意 instructions 和普通 system message 的区别——它是这个 Agent 的“人格设定”会贯穿整个运行过程而不是某一轮对话的临时指令。2.2 Runner 才是真正干活的那个循环如果说 Agent 是说明书Runner 就是那个拿着说明书去执行的人。你调用Runner.run(agent, input)的时候它内部做的事情是把 Agent 的 instructions 和用户输入组装成请求发给模型检查返回里有没有工具调用有的话执行工具、把结果拼回上下文、再发一次请求直到模型不再要求调用工具、给出最终文本回复为止。这个循环有个上限叫 max_turns防止模型陷入死循环把你的额度烧光。理解 Runner 的关键在于它维护的是完整的对话历史。每一轮工具调用的结果都会作为消息追加进去所以模型在第三步的时候能看到第一步查到了什么。这也是为什么工具返回的内容要尽量精简——你返回一个 5000 字的网页原文它就会一直占着上下文既费 token 又容易干扰判断。我一般要求工具返回结构化摘要比如只给标题、关键字段和一段不超过 200 字的摘要。2.3 Tools 的定义方式决定了模型用得准不准工具是 Agent 和外部世界交互的唯一通道。SDK 支持用装饰器把一个普通 Python 函数变成工具函数名、参数类型、docstring 会自动转成模型能理解的 schema。这里有个特别容易被忽略的点docstring 不是写给人看的是写给模型看的。模型判断该不该调用这个工具、参数怎么填全靠函数名和 docstring 的描述。我踩过的坑是写了个叫query的函数docstring 只写了“查询数据”结果模型经常在不需要的时候乱调它。后来改成search_knowledge_basedocstring 写清楚“当用户询问产品功能、价格、售后政策等知识库内已有信息时使用输入为自然语言问题”准确率立刻上来了。参数类型也要讲究。能用枚举就别用自由字符串能用必填就别设可选。模型对结构化约束的遵循程度远高于自然语言描述。比如工具需要指定查询类型与其在 docstring 里写“type 可以是 user 或 order”不如直接定义成 Literal[user, order]模型几乎不会填错。2.4 Handoffs 让多个 Agent 像接力赛一样协作Handoffs 是这套 SDK 里我觉得最有意思的设计。它允许一个 Agent 在运行过程中把控制权交给另一个 Agent交接之后由新的 Agent 继续处理并且新的 Agent 能看到之前所有的对话历史。这解决的是“一个 Agent 提示词太长、职责太杂”的问题。比如知识库场景里你可以有一个“接待 Agent”负责判断用户意图是售前咨询就交给“产品 Agent”是售后问题就交给“售后 Agent”每个 Agent 的 instructions 都很聚焦工具集也不重叠。交接和工具调用的区别在于工具调用是“我去查个东西然后回来继续”交接是“这事我不管了你接手”。所以设计时要判断清楚一个子任务是需要主 Agent 拿到结果后继续推理还是完全可以独立处理。前者用工具后者用 handoff。我个人的经验是当某个子任务的对话轮次可能超过两轮、且需要不同的工具集时就该考虑拆成独立 Agent 了。3. 动手搭建第一个可运行的 Agent3.1 环境准备与依赖安装先把环境弄干净。我强烈建议用虚拟环境因为 Agents SDK 的依赖更新比较快全局装容易和别的项目打架。Python 版本建议 3.10 以上3.9 在类型提示上会有些别扭。python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install openai-agents装完之后设置 API Key。SDK 默认读环境变量OPENAI_API_KEY你也可以在代码里显式传但环境变量更安全不会不小心提交到仓库。export OPENAI_API_KEY你的key注意不要把 key 硬编码在代码里也不要用print打印出来调试。我见过有人调试时把 key 打到了日志里结果日志被同步到了云端只能紧急轮换。用.env文件加python-dotenv是更稳妥的做法。3.2 定义一个带工具的 Agent我们从一个最简单的例子开始一个能查天气的 Agent。虽然老套但它能完整展示 Agent、Tool、Runner 三者的关系。from agents import Agent, Runner, function_tool function_tool def get_weather(city: str) - str: 查询指定城市的当前天气。 Args: city: 城市名称例如北京、上海。 # 实际项目里这里调用真实天气 API fake_data {北京: 晴25度, 上海: 多云28度} return fake_data.get(city, f暂时查不到{city}的天气) agent Agent( name天气助手, instructions你是一个天气查询助手用户问天气时调用工具查询然后用自然语言回复。, tools[get_weather], ) result Runner.run_sync(agent, 北京今天天气怎么样) print(result.final_output)跑起来之后你会看到模型自动判断出需要调用get_weather传入city北京拿到结果后组织成一句通顺的回复。整个过程你只写了工具函数和 Agent 声明那个循环完全不用管。这里有个细节值得说Runner.run_sync是同步版本适合脚本和简单场景如果是 Web 服务用异步的await Runner.run(...)更好避免阻塞事件循环。我一开始图省事全用同步结果在 FastAPI 里把整个服务卡住了排查了半天才发现是这里的问题。3.3 工具返回值的处理技巧工具返回什么直接决定了模型下一步怎么想。返回纯文本是最简单的但如果你返回的是 JSON 字符串模型也能理解而且结构化程度更高。我的习惯是返回一个简短的、字段明确的 JSON比如function_tool def search_knowledge_base(query: str) - str: 在知识库中检索相关信息。 Args: query: 用户的自然语言问题。 # 假设这里调用了向量数据库检索 results vector_search(query, top_k3) return json.dumps({ count: len(results), items: [{title: r.title, snippet: r.snippet[:200]} for r in results] }, ensure_asciiFalse)为什么要截断 snippet因为向量检索返回的原文可能很长全塞进去会迅速吃满上下文。截断到 200 字左右既保留了关键信息又给后续推理留了空间。如果模型觉得信息不够它会在下一轮再调一次工具用更精确的 query 去查这比一次性塞一大堆更高效。3.4 用 max_turns 控制成本和安全边界Runner.run有个参数叫max_turns默认值不算大但生产环境我建议显式设置。它的作用是限制“模型请求-工具执行”这个循环最多跑多少轮。设太小复杂任务做不完设太大万一模型陷入某种循环你的 token 账单会很难看。我的经验值是简单问答 3 到 5 轮足够多步检索类任务 8 到 10 轮涉及多个 Agent 交接的复杂流程可以放到 15 轮。超过这个数还没结束大概率是提示词或者工具设计有问题应该让它报错而不是继续烧钱。你可以在代码里捕获MaxTurnsExceeded异常记录下当时的对话历史这对排查问题特别有用。4. 把知识库检索接进来向量数据库与对话引擎的配合4.1 为什么知识库场景特别适合 Agent纯 RAG检索增强生成的流程是固定的用户提问、向量检索、把检索结果拼进提示词、生成回答。这个流程在简单问答上够用但一旦用户的问题需要多步推理比如“你们最贵的产品和性价比最高的产品比售后政策有什么不同”单次检索就很难覆盖——你得先查出两个产品分别是什么再分别查它们的售后政策最后做对比。这种“先想清楚要查什么、再查、再根据结果决定下一步”的过程正是 Agent 擅长的。把向量数据库作为工具接进 Agent本质上是把“检索”这个动作的决定权交给了模型。模型可以根据对话进展决定什么时候检索、用什么 query 检索、检索几次。这比固定流程灵活得多代价是延迟和成本会上升所以要在体验和开销之间找平衡。4.2 向量检索工具的参数设计一个设计良好的检索工具参数不应该只有一个 query。我通常会加上这几个query必填自然语言问题。top_k可选默认 3允许模型在需要更多候选时调大。filter可选用于按类别、时间等元数据过滤。filter 这个参数特别有用。比如知识库里有产品文档和内部流程文档你可以让模型在回答用户问题时带上filter{category: public}避免把内部信息泄露出去。这比在检索后再过滤更高效也更安全。function_tool def search_knowledge_base( query: str, top_k: int 3, category: str | None None, ) - str: 在知识库中检索信息。 Args: query: 检索用的自然语言问题尽量具体。 top_k: 返回结果数量默认3最多10。 category: 可选限定文档类别如product、policy。 top_k min(top_k, 10) results vector_search(query, top_ktop_k, categorycategory) if not results: return 未找到相关信息建议换个说法或扩大检索范围。 return json.dumps([...], ensure_asciiFalse)注意那个“未找到”的返回。很多新手会返回空字符串或者 None模型拿到空结果会一脸懵可能反复重试同一个查询。明确告诉它“没找到建议换说法”它就会调整策略比如换个关键词再试或者直接告诉用户查不到。4.3 对话引擎如何维护多轮上下文Agents SDK 的 Runner 会自动维护对话历史但如果你要做多轮对话服务需要自己把历史存下来下次请求时传进去。SDK 提供了to_input_list()方法可以把一次运行的结果转成消息列表你存到数据库或缓存里下次拼上新消息一起传给 Runner。这里有个坑历史不能无限增长。我一般只保留最近 10 轮对话更早的做摘要压缩。因为上下文越长模型越容易“分心”而且成本是线性增长的。摘要的做法是让模型把早期对话浓缩成一段话作为一条 system 消息放在最前面这样既保留了关键信息又控制了长度。4.4 检索质量差时 Agent 会怎么表现这是排查问题时的重要线索。如果向量检索返回的内容和问题不相关模型通常会做两件事之一要么硬着头皮基于错误信息编答案幻觉要么反复调用检索工具试图找到更好的结果循环。前者说明你的 instructions 里没有强调“信息不足时要如实告知”后者说明检索质量确实有问题需要回头优化 embedding 模型或分块策略。我的做法是在 instructions 里明确写“如果检索结果与问题无关不要强行回答直接告诉用户暂时没有找到相关信息并建议他们换个问法。”这句话能挡掉大部分幻觉。同时给检索工具加一个相似度阈值低于阈值的直接不返回避免垃圾信息干扰模型判断。5. 常见问题与排查技巧实录5.1 模型不调用工具怎么办最常见的原因是工具描述不够清晰模型没意识到该用它。排查步骤先看工具名和 docstring是不是太笼统再看 instructions 里有没有明确指示“遇到 X 情况必须调用 Y 工具”。有时候模型会偷懒觉得凭自己的知识就能回答这时候需要在 instructions 里强调“所有事实性信息必须来自工具检索不要依赖你的训练数据”。另一个原因是工具参数太复杂模型觉得填不对就干脆不调。解决办法是简化参数把可选参数尽量去掉必填参数用清晰的类型和描述。5.2 工具调用参数填错如果模型总是把参数填成错误的值检查类型定义。字符串类型的参数模型可能会填一个句子进去如果你期望的是短关键词就在 docstring 里写清楚“只填关键词不要填完整句子”。枚举类型能极大降低填错概率能用就用。还有一种情况是参数名有歧义。比如date和datetime模型可能搞混。改成start_date和end_date这种明确的命名问题就少了。5.3 循环调用同一个工具这通常发生在工具返回的结果模型不满意时。比如检索返回“未找到”模型不甘心换个说法再查还是没找到继续换……直到 max_turns 耗尽。解决办法是在工具返回里加入明确的终止信号比如“已尝试 3 次检索均无结果请直接告知用户”或者在 instructions 里限制“同一个工具最多调用 2 次”。5.4 交接后上下文丢失Handoff 默认会传递完整对话历史但如果你在目标 Agent 的 instructions 里写了“忽略之前的对话”那上下文就断了。检查交接目标的 instructions确保它知道自己是接着谁的工作继续做的。我一般会在目标 Agent 的 instructions 开头写一句“你接手自接待 Agent用户之前的问题是……”虽然历史里已经有但强调一下能让模型更聚焦。问题现象可能原因排查方向不调用工具描述不清、instructions 未强调检查工具名、docstring、系统提示参数填错类型模糊、命名有歧义用枚举、改明确命名循环调用结果不满意、无终止信号加终止提示、限制调用次数交接丢上下文目标 Agent 指令冲突检查交接目标的 instructions响应慢轮次过多、检索耗时长看 max_turns、优化检索性能5.5 成本失控的预防上线前一定要做压力测试模拟各种极端输入看平均轮次和 token 消耗。我一般会记录每次运行的 turns 数和 token 用量设一个告警阈值超过就人工介入。另外给工具加超时避免某个外部 API 卡住导致整个 Agent 挂起。这些看起来是运维的事但设计阶段不考虑上线后就是事故。6. 我踩过的几个真实坑第一个坑是把工具写得太“重”。我一开始把整个业务逻辑都塞进一个工具里参数十几个返回一大坨 JSON。结果模型经常填错参数而且因为返回内容太长后续推理质量明显下降。后来拆成三个小工具每个只做一件事参数不超过三个返回精简准确率和速度都上来了。工具设计的原则是“小而专”一个工具只解决一个明确的问题。第二个坑是忽略 instructions 和工具描述的重叠。有段时间我在 instructions 里写了“用 search 工具查资料”又在工具 docstring 里写了“用于查资料”结果模型有时候会困惑到底听谁的。后来我把 instructions 聚焦在“行为准则”上比如“不确定时先查再答”“查不到要如实说”把“这个工具是干什么的”完全交给 docstring职责清晰之后稳定多了。第三个坑是没有给 Agent 设边界。早期版本里用户问什么它都试图回答包括一些它根本不该碰的话题。后来在 instructions 里加了明确的“只处理与知识库相关的问题其他问题礼貌拒绝”并且用 handoff 把不相关的问题交给一个专门的“兜底 Agent”处理整个系统的可控性好了很多。这套 SDK 的学习曲线其实不陡难的是把“怎么让模型稳定地按你的预期工作”这件事想清楚。工具设计、提示词措辞、参数约束每一个细节都会影响最终效果。我建议你先用一个最小场景跑通然后逐步加工具、加 Agent、加交接每加一个就观察行为变化这样出了问题也容易定位。下一篇我会讲多 Agent 协作和交接的具体实现以及怎么用追踪功能把每次运行的内部决策过程可视化出来那才是真正能帮你调优的利器。