Skip to content

渐进式教程:三版跑起来

本页速览 用 OpenAI Agents SDK 做三个渐进项目:30 分钟跑通的天气聊天 Agent、带 todo 与文件记忆的研究助理、researcher/writer/critic 三角色多 Agent 加最小评测脚本,附三版复杂度与能力边界对照表。

渐进式教程:三版跑起来 ​

亲手造轮子那篇带你从零手写了一个最小 Agent Loop——那是对着原理练内功。这篇走另一条路:直接用生产级框架(OpenAI Agents SDK)做三个渐进式项目,每版都在上一版基础上加一块真实工程里绕不开的能力:

  • V1:单 Agent + 工具调用 + 多轮对话,一个能查真实天气的聊天机器人,目标 30 分钟跑通;
  • V2:加上规划(todo 清单)和文件记忆,变成一个「搜索 → 阅读 → 写报告」的研究助理,能扛 10 分钟以上的长任务;
  • V3:拆成 researcher / writer / critic 三个角色,再配一个 5 任务的最小 eval 脚本,让「感觉不错」变成「成功率 80%」。

三版代码都在一个项目里演化,不搞三个孤立 demo。读完你应该能把任意一版直接改造成自己的项目骨架。

为什么选 OpenAI Agents SDK

它是目前抽象最少的生产级框架之一:核心就 Agent、Runner、工具、handoff 四个原语,读完 V1 你就见完全部了。对比与选型理由见框架选型总览和 OpenAI Agents SDK 专页。本文代码基于 SDK v0.22.x(2026 年 7 月发布),API 细节均以官方文档为准。

〇、准备工作 ​

bash
mkdir agent-tutorial && cd agent-tutorial
python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install -U openai-agents httpx
export OPENAI_API_KEY=sk-...     # Windows PowerShell: $env:OPENAI_API_KEY = "sk-..."

两个注意点:

  • openai-agents 从 v0.20.0 起默认模型改为 gpt-5.6-luna(官方 release notes 明确写了这一变更)。不设 model 参数就用默认模型;本文代码一律不显式指定模型,跟随默认即可。
  • 没指定 model 不代表只能调 OpenAI——SDK 支持通过 provider 配置接入其他兼容端点,但为了让教程聚焦,本文全部用默认配置。

目录结构(三版共用):

agent-tutorial/
├── v1_weather.py      # V1:天气聊天
├── v2_researcher.py   # V2:研究助理
├── v3_team.py         # V3:多 Agent 团队
├── evals.py           # V3:最小评测脚本
└── workspace/         # V2/V3 的文件记忆与产物目录(代码自动创建)

一、V1:单 Agent + 工具,30 分钟跑通 ​

目标 ​

一个命令行聊天机器人,能回答「北京现在天气怎么样」「上海比深圳热吗」这类问题。涉及的能力点:工具定义、Agent Loop(模型自己决定先查城市经纬度再查天气)、多轮对话记忆。

架构 ​

用户输入 ──► Runner.run(agent, input, session)
                  │
                  ▼
            ┌── Agent Loop ──────────────────┐
            │  LLM 决定调 geocode_city        │
            │    → 工具返回经纬度             │
            │  LLM 决定调 get_current_weather │
            │    → 工具返回天气 JSON          │
            │  LLM 生成自然语言回答           │
            └────────────────────────────────┘
                  │
                  ▼
            final_output(同时写入 SQLiteSession)

天气数据用 Open-Meteo:免费、免 API Key、无需注册,是教学场景的理想选择。它分两个接口——geocoding(城市名 → 经纬度)和 forecast(经纬度 → 天气),正好演示「模型自主串联多个工具」。

完整代码 ​

python
# v1_weather.py
import json

import httpx
from agents import Agent, Runner, SQLiteSession
from agents.decorators import tool  # 等价于旧写法 from agents import function_tool


@tool
def geocode_city(city: str) -> str:
    """把城市名解析为经纬度。优先传英文名或拼音,如 Beijing、Shanghai。"""
    resp = httpx.get(
        "https://geocoding-api.open-meteo.com/v1/search",
        params={"name": city, "count": 1, "language": "zh"},
        timeout=10,
    )
    results = resp.json().get("results") or []
    if not results:
        return f"找不到城市 {city},请换个拼写再试"
    r = results[0]
    return json.dumps(
        {"name": r["name"], "latitude": r["latitude"], "longitude": r["longitude"]},
        ensure_ascii=False,
    )


@tool
def get_current_weather(latitude: float, longitude: float) -> str:
    """查询指定经纬度的当前天气,返回气温(°C)和天气现象代码。"""
    resp = httpx.get(
        "https://api.open-meteo.com/v1/forecast",
        params={
            "latitude": latitude,
            "longitude": longitude,
            "current": "temperature_2m,weather_code,relative_humidity_2m",
        },
        timeout=10,
    )
    return json.dumps(resp.json()["current"], ensure_ascii=False)


agent = Agent(
    name="天气助手",
    instructions=(
        "你是一个天气助手。回答天气问题时,必须先用 geocode_city 查城市经纬度,"
        "再用 get_current_weather 查实时数据,禁止凭记忆编造气温。"
        "weather_code 是 WMO 代码(0 晴,1-3 多云,45/48 雾,51-67 雨,71-77 雪,95+ 雷暴),"
        "回答时翻译成自然语言。对比多个城市时逐个查询。"
    ),
    tools=[geocode_city, get_current_weather],
)

session = SQLiteSession("weather_chat", "workspace/chat.db")  # 文件持久化的会话记忆

if __name__ == "__main__":
    print("天气助手已上线,输入 quit 退出")
    while True:
        question = input("\n你: ").strip()
        if question.lower() in {"quit", "exit"}:
            break
        result = Runner.run_sync(agent, question, session=session)
        print(f"助手: {result.final_output}")

运行效果 ​

你: 北京现在天气怎么样
助手: 北京当前气温 32.4°C,有小雨(毛毛雨),出门建议带伞。

你: 那上海呢,比北京热吗      ← session 记住了上文的城市
助手: 上海当前 34.1°C,多云。比北京(32.4°C)热约 1.7 度。

三个细节值得停下来看一眼:

  1. 工具的 docstring 就是给模型看的接口文档。geocode_city 的 docstring 里写「优先传英文名或拼音」,是在教模型怎么构造参数——这是 prompt 的一部分,不是注释。写法细节见 Prompt Engineering。
  2. 你一行 loop 都没写。Runner.run 内部就是完整的 Agent Loop:调模型 → 有 tool call 就执行并把结果塞回上下文 → 再调模型 → 直到模型输出纯文本。可以对比手写版看看框架替你封装了什么。
  3. SQLiteSession 一行解决多轮记忆。Runner 每次运行前自动读历史、运行后自动写新条目,数据库落在 workspace/chat.db,重启进程记忆还在。更多记忆策略见记忆系统。

工具返回 JSON 字符串,不要返回 Python 对象

工具返回值会被序列化进对话历史。返回 json.dumps(...) 的字符串最稳:模型读得懂,session 存得下。返回自定义对象或 dict 在某些版本里会触发序列化警告。

本版学会什么 ​

  • Agent = 名字 + instructions + 工具列表,没有更多魔法;
  • @tool 装饰器从函数签名和 docstring 自动生成 JSON Schema;
  • Runner.run / run_sync 封装了完整的 tool-calling 循环;
  • SQLiteSession 提供开箱即用的多轮记忆。

V1 的能力边界也很清楚:没有规划。你让它「调研一下 Agent 框架现状并写份报告」,它会手忙脚乱地搜两轮就草草交差——因为没有机制让它把大任务拆成步骤、记住中间产物。这就是 V2 要补的。

二、V2:加规划与记忆的研究助理 ​

目标 ​

输入一个研究课题(如「2026 年多智能体框架选型对比」),Agent 自主完成:拆解子问题 → 逐条联网搜索 → 阅读原文 → 把笔记落到磁盘 → 汇总成 markdown 报告。整个任务可能跑十几轮工具调用,中途挂了重启还能接着干。

这一版引入三个新机制:

  • todo 规划:强制 Agent 先把计划写成 checklist 文件,每完成一步更新状态;
  • 文件记忆:搜索结果和阅读笔记写入 workspace/,而不是全堆在 context window 里——这是长任务不「忘事」的关键;
  • 联网工具:用 SDK 内置的 WebSearchTool(OpenAI 托管的网页搜索),加一个自写的 read_url 抓正文。

架构 ​

研究课题
   │
   ▼
┌─ Research Agent (max_turns=25) ──────────────────────┐
│ 1. update_todo: 把课题拆成 4-6 个子问题 → plan.md     │
│ 2. 循环: web_search → read_url → save_note           │
│         (笔记落盘 workspace/notes/*.md)             │
│ 3. 全部子问题完成后,读回笔记 → 写 report.md          │
└───────────────────────────────────────────────────────┘
   │
   ▼
workspace/report.md(最终交付物)

context window 不是记忆

V1 里所有信息都活在对话历史里,这在 10 轮以内没问题;但研究任务动辄几十轮工具调用、几万 token 的网页正文,全塞上下文既贵又会触发「中间遗忘」。V2 的原则是:原始材料进文件,上下文里只留指针(文件路径 + 摘要)。这就是 Context Engineering 的核心思想,也是 Claude Code、Manus 这类长任务产品的共同做法。

关键代码骨架 ​

工具层(文件即记忆,刻意不用任何向量库):

python
# v2_researcher.py(节选,工具部分完整)
import json
import re
from pathlib import Path

import httpx
from agents import Agent, Runner, WebSearchTool
from agents.decorators import tool

WORKSPACE = Path("workspace")
NOTES = WORKSPACE / "notes"


@tool
def update_todo(plan_markdown: str) -> str:
    """写入或更新任务计划。plan_markdown 是完整计划,
    用 - [ ] / - [x] 表示待办和已完成。每开始或完成一个子任务都要调用一次。"""
    WORKSPACE.mkdir(exist_ok=True)
    (WORKSPACE / "plan.md").write_text(plan_markdown, encoding="utf-8")
    return "计划已更新到 workspace/plan.md"


@tool
def read_url(url: str) -> str:
    """抓取网页正文(粗提取,截断到 6000 字符),用于深入阅读搜索结果。"""
    try:
        resp = httpx.get(url, timeout=15, follow_redirects=True)
        resp.raise_for_status()
    except Exception as e:
        return f"抓取失败: {e}"
    text = re.sub(r"<(script|style)[^>]*>.*?</\1>", "", resp.text, flags=re.S)
    text = re.sub(r"<[^>]+>", " ", text)          # 去 HTML 标签
    text = re.sub(r"\s+", " ", text).strip()
    return text[:6000]                             # 宁截断不塞爆上下文


@tool
def save_note(filename: str, content: str) -> str:
    """把一条研究笔记保存到 workspace/notes/。filename 不含路径和扩展名。"""
    NOTES.mkdir(parents=True, exist_ok=True)
    (NOTES / f"{filename}.md").write_text(content, encoding="utf-8")
    return f"笔记已保存: notes/{filename}.md"


@tool
def read_all_notes() -> str:
    """读取 notes 目录下全部笔记(写报告前调用一次)。"""
    if not NOTES.exists():
        return "还没有任何笔记"
    parts = [f"===== {p.name} =====\n{p.read_text(encoding='utf-8')}"
             for p in sorted(NOTES.glob("*.md"))]
    return "\n\n".join(parts)


@tool
def write_report(content: str) -> str:
    """把最终研究报告写入 workspace/report.md。"""
    WORKSPACE.mkdir(exist_ok=True)
    (WORKSPACE / "report.md").write_text(content, encoding="utf-8")
    return "报告已写入 workspace/report.md"

Agent 定义——注意 instructions 里把「工作流纪律」写死了,这是让 Agent 不乱来的主要手段:

python
# v2_researcher.py(Agent 与入口部分)
agent = Agent(
    name="研究助理",
    instructions=(
        "你是一个严谨的研究助理。收到研究课题后严格执行以下流程:\n"
        "1. 先调用 update_todo,把课题拆成 4-6 个可独立搜索的子问题,写成 checklist;\n"
        "2. 逐个研究子问题:web_search 找来源 → 对最有价值的 1-2 个链接用 read_url 读原文 "
        "→ 用 save_note 把关键事实(含来源 URL)存成笔记 → update_todo 勾选完成;\n"
        "3. 全部子问题完成后,调用 read_all_notes 汇总,用 write_report 输出结构化报告:"
        "摘要、分主题论述(标注来源)、争议点与不确定性、参考资料列表;\n"
        "4. 最终回复只需简述结论和报告路径。\n"
        "原则:所有具体事实必须来自搜索到的来源,禁止凭记忆编造数字和日期;"
        "笔记里每条事实都要带来源 URL。"
    ),
    tools=[WebSearchTool(), update_todo, read_url, save_note, read_all_notes, write_report],
)

if __name__ == "__main__":
    import asyncio
    topic = input("研究课题: ").strip()
    result = asyncio.run(Runner.run(agent, topic, max_turns=25))  # 长任务必须放宽轮次上限
    print(result.final_output)

几个设计决策解释一下:

  • max_turns=25 是安全带,不是目标值。一轮 = 一次模型调用。研究任务十几轮很正常,但必须有上限防止死循环烧 token。V1 没设是因为聊天任务 3-5 轮就结束了。
  • read_url 是教学版,正文提取用正则糊弄,遇到 JS 渲染的页面会抓空。生产里换 trafilatura、jina reader 或浏览器渲染,接口签名不用变。
  • todo 用文件而不是内存变量,换来一个便宜但实用的能力:进程中断后重跑,Agent 读一下 plan.md 和 notes/ 就知道干到哪了。更系统的规划模式(ReAct、Plan-and-Execute 等)见规划与推理。

运行效果 ​

研究课题: 对比 2026 年主流 Agent 框架的选型建议

(Agent 内部:update_todo 拆出 5 个子问题 → 搜索+阅读 8 个页面
  → 存了 6 条笔记 → 生成报告)

调研完成。核心结论:LangGraph 适合需要精细状态控制的复杂编排,
OpenAI Agents SDK 适合快速落地......完整报告见 workspace/report.md。

workspace/ 下会留下 plan.md(带勾选状态的清单)、notes/(6 条带来源的笔记)、report.md(最终报告)——过程产物全部可审计,出问题能定位是哪一步搜错了。

本版学会什么 ​

  • 长任务 = 规划(todo)+ 外部记忆(文件)+ 轮次上限,三者缺一不可;
  • instructions 是工作流纪律的载体,写得越像 SOP,Agent 越稳;
  • 上下文里放指针、磁盘上放原文,是长任务的生存法则;
  • 但「一个 Agent 干全流程」开始显形了:搜索、写作、审稿挤在一个 context 里,提示词越来越长,质量互相拖累。这是 V3 拆角色的动机。

三、V3:多 Agent 与最小评测 ​

目标 ​

把 V2 的单 Agent 拆成三个角色,并回答一个此前一直在回避的问题:它到底有多靠谱?

  • Researcher:只负责搜索和读原文,输出带来源的事实清单;
  • Writer:只负责把事实清单组织成报告,不允许自己编造事实;
  • Critic:只负责审稿——检查事实是否有来源支撑、结构是否完整,打回或放行。

同时写一个 evals.py:5 个固定测试任务,每个任务有预设的通过标准,跑完输出成功率。这是从「玩具」走向「工程」的分水岭。

架构 ​

SDK 提供两种多 Agent 编排:handoff(分诊 Agent 把对话权整个交给专家)和 agents as tools(经理 Agent 把专家当工具调用,自己始终掌控最终答案)。研究报告场景需要有人对最终产物负责,选后者:

                用户课题
                   │
                   ▼
        ┌──── Orchestrator ────┐
        │  tools:              │
        │   research(...)  ────┼──► Researcher ──► WebSearchTool / read_url
        │   write_report(...) ─┼──► Writer      (无工具,只写)
        │   critique(...)  ────┼──► Critic     (无工具,只审)
        └──────────────────────┘
                   │   不满意则把批评意见发回 Writer 重写(≤2 轮)
                   ▼
              workspace/report.md

为什么不让三个 Agent 自由 handoff?因为研究 → 写作 → 审稿是有向流程,不是路由问题。让 LLM 自主决定顺序(agents as tools)已经够用;如果你对确定性要求更高,V3 的代码完全可以改成三次 Runner.run 顺序调用——见下文取舍讨论。

关键代码骨架 ​

三个专家 Agent(各自 instructions 极短且职责单一,这是拆角色的核心收益):

python
# v3_team.py(节选:角色定义)
from agents import Agent, Runner, WebSearchTool
from agents.decorators import tool
# read_url、write_report 从 v2 复用:from v2_researcher import read_url, write_report

researcher = Agent(
    name="Researcher",
    instructions=(
        "你只做调研。围绕课题搜索并阅读原始来源,输出事实清单:"
        "每条事实一句话 + 来源 URL。不做评论,不写成文章。"
    ),
    tools=[WebSearchTool(), read_url],
)

writer = Agent(
    name="Writer",
    instructions=(
        "你只做写作。把给定的事实清单组织成结构化的中文研究报告:"
        "摘要、分主题论述、争议点、参考资料。"
        "严禁添加事实清单之外的任何具体事实、数字或日期;"
        "收到修改意见时逐条响应并输出完整新版。"
    ),
    tools=[write_report],
)

critic = Agent(
    name="Critic",
    instructions=(
        "你是严格的审稿人。检查报告:"
        "1) 每个具体事实是否有来源支撑;2) 结构是否完整;3) 有无夸大和编造。"
        "输出 VERDICT: PASS 或 VERDICT: FAIL,FAIL 时必须给出具体、可执行的修改意见。"
    ),
)

编排者把三个角色当工具调用:

python
# v3_team.py(节选:编排与入口)
orchestrator = Agent(
    name="Orchestrator",
    instructions=(
        "你协调一个研究写作团队。流程:\n"
        "1. 调 research 获取事实清单;\n"
        "2. 调 write_report(Writer)基于事实清单写初稿并落盘;\n"
        "3. 调 critique 审稿;\n"
        "4. 若 FAIL,把批评意见连同事实清单发回 write_report 重写,最多重写 2 次;\n"
        "5. 最终回复:报告路径 + 审稿结论 + 重写次数。"
    ),
    tools=[
        researcher.as_tool(tool_name="research",
                           tool_description="调研课题,返回带来源的事实清单"),
        writer.as_tool(tool_name="write_report",
                       tool_description="基于事实清单写报告(可带修改意见重写)"),
        critic.as_tool(tool_name="critique",
                       tool_description="审查报告质量,返回 PASS/FAIL 和意见"),
    ],
)

if __name__ == "__main__":
    import asyncio
    topic = input("研究课题: ").strip()
    result = asyncio.run(Runner.run(orchestrator, topic, max_turns=20))
    print(result.final_output)

agent.as_tool() 把子 Agent 包装成一个普通工具:Orchestrator 看到的是三个「函数」,底层每次调用都是一次独立的子 Agent 运行(有自己干净的上下文)。上下文隔离正是拆角色的工程价值——Researcher 读过的几万字网页不会污染 Writer 的窗口。

多 Agent 在大多数情况下是过度设计

V3 拆三个角色,是因为「调研-写作-审稿」的上下文和提示词确实互相干扰。如果你的任务单 Agent + 好提示词就能稳定通过评测,不要拆——每多一个 Agent 就多一轮 LLM 调用、多一份 prompt 维护成本、多一处失败点。什么时候该拆的完整讨论见多智能体架构。

evals.py:最小评测脚本 ​

不搞框架,30 行:5 个任务、关键词判据、成功率统计。

python
# evals.py
import asyncio

from agents import Runner
from v3_team import orchestrator

# (任务, 通过判据——报告中应出现的关键词组,全部命中算过)
TASKS = [
    ("调研 Model Context Protocol 的设计目标与传输方式",
     ["MCP", "stdio"]),
    ("对比 LangGraph 与 OpenAI Agents SDK 的适用场景",
     ["LangGraph", "OpenAI Agents SDK"]),
    ("调研 RAG 系统中 chunk 切分的主流策略",
     ["chunk", " embedding"]),
    ("总结 ReAct 论文的核心思想",
     ["ReAct", "推理"]),
    ("调研 2026 年 AI Agent 评测的主流 benchmark",
     ["benchmark"]),
]


async def run_eval():
    passed = 0
    for i, (task, keywords) in enumerate(TASKS, 1):
        result = await Runner.run(orchestrator, task, max_turns=20)
        report = open("workspace/report.md", encoding="utf-8").read()
        missing = [k for k in keywords if k.lower() not in report.lower()]
        ok = not missing
        passed += ok
        print(f"[{i}/5] {'PASS' if ok else 'FAIL (缺: ' + ','.join(missing) + ')'} | {task[:20]}...")
    print(f"\n成功率: {passed}/{len(TASKS)} = {passed / len(TASKS):.0%}")


if __name__ == "__main__":
    asyncio.run(run_eval())

典型输出:

[1/5] PASS | 调研 Model Context Proto...
[2/5] PASS | 对比 LangGraph 与 OpenAI ...
[3/5] FAIL (缺: embedding) | 调研 RAG 系统中 chunk ...
[4/5] PASS | 总结 ReAct 论文的核心思想...
[5/5] PASS | 调研 2026 年 AI Agent 评...

成功率: 4/5 = 80%

这个脚本的幼稚是刻意的——关键词匹配会误判(报告写了「向量」没写「embedding」就 FAIL)。但它的价值不在精度,在于让你第一次拥有「改动前后可对比」的基线:改了 Orchestrator 的提示词?跑一遍 evals.py,成功率从 60% 到 80% 就是真改进,掉到 40% 就是改坏了。没有它,你所有的「优化」都是玄学。下一步的自然演化方向:关键词换成 LLM-as-judge、任务集扩到 30+、接入 Langfuse 之类的平台——见评测体系和评测实战。

eval 任务集本身会过拟合

只有 5 个任务时,你很容易不自觉地把提示词调成「刚好通过这 5 题」。规则:任务集只加不改,定期补充新题;调提示词后如果通过率涨了但体感没变,先怀疑任务集。

本版学会什么 ​

  • agents as tools:经理 Agent 掌控全局,专家 Agent 各有干净上下文;
  • 拆角色的本质是上下文隔离和提示词聚焦,不是「人多力量大」;
  • 最小 eval = 固定任务集 + 机器可判的通过标准 + 成功率,30 行就够起步;
  • Critic 打回重写是最便宜的 self-improvement 回路。

四、三版对照:复杂度、代码量与能力边界 ​

维度V1 天气聊天V2 研究助理V3 多 Agent 团队
代码量(约)60 行120 行150 行 + 30 行 eval
Agent 数114(编排者 + 3 专家)
工具数2(自写)5 自写 + WebSearchTool2 自写 + WebSearchTool + 3 个 agent 工具
规划能力无todo 文件 checklist编排流程内置在 instructions
记忆SQLiteSession(对话历史)文件系统(笔记/计划/报告)文件系统 + 子 Agent 上下文隔离
单次任务轮次3-5 轮10-25 轮15-40 轮(含子 Agent)
典型耗时秒级3-10 分钟5-15 分钟
质量保障无来源标注可人工抽查Critic 审稿 + eval 成功率
能力边界单点事实问答单一课题调研,深度有限可分工的长任务,但协调成本上升
主要失败模式工具参数错误搜不到好来源、笔记断档编排者跳过审稿、子 Agent 输出格式漂移

一条贯穿三版的主线:每一版加的复杂度都对应一个具体的失败模式。V2 加文件记忆是因为长任务上下文装不下;V3 拆角色是因为单 Agent 提示词互相干扰;eval 是因为「感觉不错」不可信。如果你的场景没遇到那个失败模式,就停在对应版本——这是工程判断,不是偷懒。

五、下一步路线 ​

三版跑通之后,按你的目标选方向:

  • 想继续做项目:把 V3 改造成自己的作品——换一个垂直领域(投研、竞品监控、文献综述),加上人工确认环节(Human-in-the-Loop)和成本统计(成本优化)。更多选题见作品集项目。
  • 想深入框架:对照读 LangGraph(显式状态图,适合复杂编排)和 Claude Agent SDK,体会「框架替你管理多少状态」这条光谱。
  • 想补原理:回到核心组件系列,把这篇用到的概念(Agent Loop、context engineering、planning、memory)逐个吃透;再读 ReAct 等核心论文。
  • 想找工作:把 V3 + eval 的经历写成简历上的量化成果(「搭建多 Agent 研究系统,自建 5 任务评测集,迭代提示词使成功率从 60% 提升到 80%」),具体写法见简历诊断。

无论走哪条,evals.py 里的思维都留着:先定义「怎样算好」,再动手优化。

参考资料 ​