
May 1, 2026 · 9:17 AM
OpenAI Agents SDK #10:99% 的开发者都搞错了——Context 到底传没传给 LLM?
从「context 对象传了却对 LLM 无效」这个高频 bug 切入,系统拆解 OpenAI Agents SDK 的 Context 双轨设计:本地 RunContextWrapper 与 LLM-visible Context 的本质边界、ToolContext 的 5 个工具级元数据属性、多 Agent Handoff 下 Context 单例自动流转机制,附两个完整带注释代码示例(基础用法 + 客服多 Agent 流转),结尾给出 3 条立即可用的实践建议。
你有没有遇到过这种情况:把用户信息塞进
context 对象,满以为 Agent 能「记住」用户是谁,结果 LLM 给出的回答毫无个性化,完全不知道上下文里装了什么?这不是 bug。这是你对 Context 的理解出了根本性偏差。
OpenAI Agents SDK 里的 Context 其实分成两个完全独立的世界:一个给你的代码用,另一个给 LLM 看。搞混这两者,是 Agent 开发者最常踩的坑之一1。
一句话说清楚本质
本地 Context(Local Context):你在 Python 代码里读写的状态容器。工具函数、生命周期钩子、Handoff 回调都能访问,但 LLM 看不见。
LLM-visible Context:真正进入模型提示词(Prompt)的内容——Agent 的
instructions、Runner.run() 的 input 参数、函数工具的返回值、检索结果。这条分界线,是整个 Context 机制的设计核心。

RunContextWrapper:本地 Context 的载体
RunContextWrapper[TContext] 是 SDK 对本地 Context 的泛型封装,把你的自定义业务对象在整个运行链路里透传1。Loading stats card…
重点看
wrapper.context:这里存的是你自定义的 Python 对象,比如用户 ID、数据库连接池、租户信息,任何你需要在代码里用的东西2。它不会被序列化、不会进 prompt,只在本地 Python 进程内流转。wrapper.usage特别有用——它聚合了单次运行里所有 Agent 消耗的 token 总量,省去你自己累加的麻烦。
代码示例一:基础用法
下面是一个完整可运行的例子,展示如何定义 Context 类型、如何在工具里读取 Context1:
from dataclasses import dataclass
from agents import Agent, Runner, function_tool
from agents.run_context import RunContextWrapper
# 1. 定义你的业务状态对象
@dataclass
class UserInfo:
name: str
uid: int
is_premium: bool = False
# 2. 工具函数接收 RunContextWrapper[UserInfo],获取本地 Context
@function_tool
def greet_user(wrapper: RunContextWrapper[UserInfo]) -> str:
"""向当前登录用户打招呼"""
user = wrapper.context # 拿到 UserInfo 实例
tier = "高级会员" if user.is_premium else "普通用户"
return f"你好,{user.name}!你当前是{tier}(UID: {user.uid})"
# 3. Agent 标注泛型类型,绑定 UserInfo
agent: Agent[UserInfo] = Agent(
name="PersonalAssistant",
instructions="你是一个个人助理,可以通过工具获取用户信息来提供服务。",
tools=[greet_user],
)
# 4. 运行时把 UserInfo 实例传给 context 参数
user_info = UserInfo(name="张三", uid=10086, is_premium=True)
result = Runner.run_sync(
starting_agent=agent,
input="请帮我打个招呼",
context=user_info, # 整次运行内所有工具都能访问
)
print(result.final_output)
# 示例输出:你好,张三!你当前是高级会员(UID: 10086)
# 5. 查看 token 消耗(wrapper.usage 聚合了全部 Agent)
# result.usage.total_tokens 可直接查看注意第 3 步:
Agent[UserInfo] 是泛型标注,让类型检查器(mypy / pyright)在编译期捕获类型不匹配——比如你把 OrderInfo 传给一个期望 UserInfo 的 Agent,立刻报错1。关键提醒:
greet_user 里的 wrapper.context.name 是 Python 本地读取,对 LLM 完全透明。如果你想让模型「知道」用户叫什么,必须把用户名写入 instructions 或通过工具返回值暴露出来。ToolContext:工具级元数据的精准入口
ToolContext 是 RunContextWrapper 的子类,在工具执行和工具生命周期钩子里可用,额外暴露:| 属性 | 含义 |
|---|---|
tool_name | 当前工具名称(如 "search_web") |
tool_call_id | 此次调用的唯一 ID,可用于日志追踪 |
tool_arguments | LLM 传入的原始参数 JSON 字符串 |
tool_namespace | 工具所属命名空间 |
qualified_tool_name | 带命名空间修饰的完整工具名 |
典型用途:在审计日志里精确记录「哪次运行、哪个工具、哪次调用」,或者在工具钩子里根据
tool_call_id 实现幂等控制1。代码示例二:多 Agent Handoff 下的 Context 共享
Context 最有意思的特性:它在整个运行链路里是「单例传递」的。一次
Runner.run() 调用所有涉及的 Agent,共享同一个 Context 实例。这意味着,Agent A 把某个状态写入
wrapper.context,Agent B 接手后可以直接读到3:from dataclasses import dataclass, field
from typing import List
from agents import Agent, Runner, function_tool, handoff
from agents.run_context import RunContextWrapper
@dataclass
class CustomerServiceContext:
customer_id: str
visited_agents: List[str] = field(default_factory=list) # 追踪流转路径
resolved: bool = False
# 前台分流 Agent 工具:记录首次接触
@function_tool
def log_triage(wrapper: RunContextWrapper[CustomerServiceContext]) -> str:
"""记录分流信息"""
ctx = wrapper.context
ctx.visited_agents.append("triage") # 直接修改 Context,下游 Agent 能看到
return f"客户 {ctx.customer_id} 已记录,准备分流"
# 退款 Agent 工具:读取并更新 Context
@function_tool
def process_refund(wrapper: RunContextWrapper[CustomerServiceContext]) -> str:
"""处理退款"""
ctx = wrapper.context
ctx.visited_agents.append("refund") # 追加到路径链
ctx.resolved = True
return f"退款已处理,服务路径:{' → '.join(ctx.visited_agents)}"
# 定义退款专项 Agent
refund_agent: Agent[CustomerServiceContext] = Agent(
name="RefundAgent",
instructions="你专门处理退款请求,使用 process_refund 工具完成操作。",
tools=[process_refund],
)
# 前台分流 Agent,完成分流后 Handoff 给退款 Agent
triage_agent: Agent[CustomerServiceContext] = Agent(
name="TriageAgent",
instructions="你是前台客服,先记录分流信息,然后把退款请求转给退款专员。",
tools=[log_triage],
handoffs=[refund_agent], # 声明可交接的目标 Agent
)
# 执行:Context 在 TriageAgent → RefundAgent 间自动流转
ctx = CustomerServiceContext(customer_id="C-2025-001")
result = Runner.run_sync(
starting_agent=triage_agent,
input="我要申请退款",
context=ctx,
)
# RefundAgent 处理完后,ctx 里记录了完整路径
print(ctx.visited_agents) # ['triage', 'refund']
print(ctx.resolved) # True注意这里没有任何额外的「Context 传递」代码。Handoff 发生时,SDK 自动把同一个
context 对象透传给下一个 Agent3。Runner.run() 调用涉及的所有 Agent(包括通过 Handoff 链接的)必须使用相同的 TContext 类型。如果 TriageAgent 用 CustomerServiceContext,而 RefundAgent 用 OrderContext,运行时会类型错误。这是 SDK 的强制约束,不是可以绕过的限制1。 Handoff 下的进阶:input_filter 与 on_handoff
Context 共享只是 Handoff 状态管理的一面。如果你还需要控制「传给下一个 Agent 的对话历史里有什么」,就需要
input_filter3。from agents.handoffs import HandoffInputData
from agents import handoff
def slim_history_filter(data: HandoffInputData) -> HandoffInputData:
"""
只保留最近 3 条消息,避免退款 Agent 接收过多无关上下文。
data.run_context 里有 RunContextWrapper,可以读写 Context。
"""
recent_items = list(data.new_items)[-3:]
return data.clone(new_items=tuple(recent_items))
refund_handoff = handoff(
agent=refund_agent,
input_filter=slim_history_filter, # 过滤传入历史
on_handoff=lambda ctx, _: print( # 交接时触发回调
f"即将移交,当前路径:{ctx.context.visited_agents}"
),
)「本地 Context 和 LLM-visible Context」的边界在哪里
这是最值得反复确认的认知模型。
本地 Context 能做什么:存数据库连接池、当前登录用户 ID、请求追踪 ID(trace_id)、A/B 实验分组、中间计算结果、已执行工具的幂等记录。一句话,任何「你的代码需要但不想放进 prompt」的东西都可以放这2。
LLM 如何获知这些信息:通过四条路径——
instructions(静态或动态函数生成)Runner.run(input=...)传入的初始消息- 函数工具的返回值(工具把 Context 里的数据格式化成字符串返回)
- 检索结果注入
这个设计不是限制,而是安全边界。密钥、PII、数据库连接等信息留在本地 Context,永远不会意外地出现在发给 LLM 的请求里1。
护栏中的 Context 复用
提一个容易被忽略的点:护栏(Guardrails)函数也接收
RunContextWrapper,而且可以在验证逻辑里嵌套调用子 Agent——直接把原 Context 传进去5:@input_guardrail
async def policy_check(
ctx: RunContextWrapper[CustomerServiceContext], # 拿到同一个 Context
agent: Agent,
input: str,
) -> GuardrailFunctionOutput:
# 嵌套调用验证 Agent,复用当前 Context
result = await Runner.run(
validation_agent,
input=input,
context=ctx.context, # 透传,避免重新构建
)
triggered = "违规" in result.final_output
return GuardrailFunctionOutput(
output_info=result.final_output,
tripwire_triggered=triggered,
)这种模式让「需要调用 LLM 做语义判断」的护栏也能访问完整的业务 Context,不用把校验逻辑和状态管理拆成两套体系5。
3 条可立即落地的实践建议
1. 用 dataclass 或 Pydantic 定义 Context,不要用 dict
裸
dict 意味着放弃类型检查。用 @dataclass 或 BaseModel 定义 Context 类,配合 Agent[YourContext] 泛型标注,mypy/pyright 会在 CI 里替你挡住类型不匹配的问题1。2. 让工具「翻译」Context,而不是让 LLM「猜」Context
需要 LLM 了解某个 Context 字段?写一个工具函数读取该字段并以自然语言返回。这比手动拼接
instructions 灵活,也更容易测试和维护6。3. 多 Agent Handoff 链路中,用 Context 而非对话历史传递结构化状态
对话历史是给 LLM 看的文本流,不适合存结构化数据(JSON 塞到 prompt 里很快吃光 token)。结构化的跨 Agent 状态(已处理的工单 ID、当前流程步骤、用户权限等级)放进 Context 对象,用
wrapper.context.field 读写,干净且不消耗 token3。预告 #11
下一篇我们进入 Models 模块——SDK 如何抽象不同模型提供商、
RunConfig.model 和 Agent.model 的优先级覆盖逻辑、如何接入非 OpenAI 模型(Anthropic、Gemini、本地 Ollama),以及 ModelSettings 里 temperature/top_p 的覆盖机制。如果你想把同一套 Agent 代码同时跑在 GPT-5 和 Claude 4 上做对比测试,下篇给你完整方案。
封面图:AI 生成(深蓝渐变技术风,RunContextWrapper 数据流架构示意)
More from this channel
- OpenAI Agents SDK #14:Agent 刚要删库,你却毫无感知——Human in the Loop 全解析
- OpenAI Agents SDK #13:每次多轮对话都要手写 `.to_input_list()`?Sessions 帮你彻底告别这个坑
- OpenAI Agents SDK #12:你的 Agent 跑到第 8 轮突然停了——你却看不到任何错误信息
- OpenAI Agents SDK #11:多模型调度背后,你不知道的优先级覆盖链
- OpenAI Agents SDK #9:让 Agent「边跑边说」——Streaming 流式输出全解析
- OpenAI Agents SDK #8:为 Agent 装上「双保险」——Guardrails 防护栏全解析
- OpenAI Agents SDK #7:Tracing——让 Agent 的每一步都「可见」
- OpenAI Agents SDK #6:把 Agent 关进「安全箱」——Sandbox 执行环境全解析
Related content
- Sign in to comment.