openai-agents-python 上下文管理实战:本地上下文(RunContextWrapper/ToolContext)与 LLM 上下文注入全解析
【免费下载链接】openai-agents-pythonA lightweight, powerful framework for multi-agent workflows项目地址: https://gitcode.com/GitHub_Trending/op/openai-agents-python
本文是 openai-agents-python 框架中「上下文管理」的完整技术指南。围绕 docs/ja/context.md(英文原版见 docs/context.md),系统讲解两大类上下文:一类是代码本地可用的上下文(通过RunContextWrapper与ToolContext传递的数据与依赖),另一类是LLM 可见的上下文(如何把新数据注入对话历史供模型参考)。读完本文,你将掌握如何在工具函数、on_handoff回调、生命周期钩子中读写上下文、用上下文做能力可见性(capability visibility)控制、在嵌套Agent.as_tool()场景下共享状态,以及四种把数据喂给 LLM 的标准姿势。
两种上下文:先分清「谁在看」
在 openai-agents-python 中,“上下文(context)”是一个被重度重载(overloaded)的术语。梳理整个框架,你需要关心的上下文只有两大类:
- 代码本地可用的上下文:工具函数执行时、
on_handoff等回调内、生命周期钩子(lifecycle hooks)中所需要的数据与依赖。例如用户信息、日志器对象、数据抓取器。 - LLM 可用的上下文:模型生成响应时能够引用到的数据,也就是最终出现在对话历史(conversation history)里的内容。
前者是你的 Python 代码在运行时直接持有的对象,后者是模型在推理时能看到的文本。二者互不重叠:本地上下文对象永远不会被发送给 LLM。本文先讲本地上下文,再讲如何把数据送入 LLM 视野。
本地上下文:RunContextWrapper与context属性
本地上下文由RunContextWrapper类及其内部的context属性表示。它的工作方式只有三步:
- 创建任意 Python 对象作为上下文。常见做法是用 dataclass 或 Pydantic 对象(任意类型都行)。
- 把该对象通过各类运行方法传入,例如
Runner.run(..., context=whatever)。 - 所有工具调用、生命周期钩子等都会收到一个包装对象
RunContextWrapper[T],其中T是上下文对象的类型;对象本体通过wrapper.context访问。
从源码看,Runner.run的签名是async def run(..., context: TContext | None = None),上下文类型由泛型TContext承载(定义于 run_context.py)。框架内部会在执行链中把用户传入的对象包装进RunContextWrapper,再分发给各个环节。
上下文能装什么
官方文档明确推荐的用途有三类:
- 运行相关的上下文数据:例如用户名、UID 以及其他用户信息;
- 依赖(Dependencies):例如 logger 对象、数据抓取器(data fetchers)等;
- 辅助函数(Helper functions):可以放进上下文对象里,在工具中直接调用。
最重要的一条规则:同一次运行必须使用同类型上下文
最需要注意的一点:针对某一次 Agent 运行的所有Agent、工具函数、生命周期处理等,必须使用同一种类型的上下文。
这一约束的工程价值在于类型安全。结合 tool.py 可以看到,FunctionTool.is_enabled的签名是Callable[[RunContextWrapper[Any], AgentBase], MaybeAwaitable[bool]],如果你在同一个 Agent 上混用接收不同上下文类型的工具,类型检查器(如 mypy/pyright)会在编译期报错,从根源上避免运行时AttributeError。
危险提示:上下文不会发给 LLM
!!! danger "注意" 上下文对象是不会被发送给 LLM的。它纯粹是一个本地对象,你可以读取它的值、写入新值、调用它的方法。
这一点也是 run_context.py 的 docstring 所强调的:上下文是把依赖和数据传递给你写的代码(工具函数、回调、钩子等)的通道,而不是喂给模型的通道。
单次运行内的状态共享语义
在一次运行内部,所有派生出来的 wrapper共享同一个底层应用上下文、批准状态(approval state)与用量追踪(usage tracking)。从 run_context.py 的_share_tool_state_with可以看到,派生 wrapper 会直接共享_approvals与_tool_invocations字典引用,这意味着子运行中的批准/拒绝决定和调用记账会立即反映到父运行。
特别地,嵌套的Agent.as_tool()运行可能会附带一个不同的tool_input(结构化输入,见下文),但默认情况下不会为你的应用状态创建独立副本。也就是说,你在嵌套运行里对wrapper.context的修改会影响到外层——这一点在多 Agent 协作场景中务必留意。
用本地上下文控制能力可见性(Capability Visibility)
当函数工具(function tools)、MCP 工具和 handoff(交接)依赖同一个请求策略时,正确的做法是:把策略的输入值或辅助函数放到你的应用上下文(application context)上,而不是为每个功能单独维护一份能力列表。SDK 的各个接口都通过各自的回调向代码暴露当前运行上下文:
| SDK 接口 | 回调签名中的上下文 | 源码位置 |
|---|---|---|
FunctionTool.is_enabled | 接收RunContextWrapper(外加AgentBase) | tool.py |
Handoff.is_enabled | 接收RunContextWrapper(外加AgentBase) | handoffs/init.py |
MCP 的tool_filter | 接收ToolFilterContext,其run_context属性包含当前的RunContextWrapper | mcp/util.py |
在 mcp/util.py 中,ToolFilterContext被定义为包含run_context(当前运行上下文)、agent(请求工具列表的 Agent)和server_name(MCP 服务器名)三个字段的 dataclass,即ToolFilterCallable = Callable[[ToolFilterContext, MCPTool], MaybeAwaitable[bool]]。
必须理解的能力边界:这些回调只控制 SDK 在本次运行中暴露哪些能力(工具是否出现、handoff 是否可选),它们不能对模型生成的参数或资源选择进行授权。因此:
- 函数工具:授权判断应在工具实现内部执行,或视需要使用工具输入护栏(tool input guardrails)与批准(approvals / human-in-the-loop);
- MCP 服务器:必须自行授权其受保护的操作(SDK 无法替服务器做授权);
- 带
input_type的 handoff:应在on_handoff的开头(应用产生副作用之前)检查解析后的输入,授权失败时直接抛出异常而不是返回值。注意工具输入护栏不会在 handoff 上执行,相关回调生命周期见 handoffs.md 的 Handoff inputs 一节。
RunContextWrapper暴露了哪些信息
RunContextWrapper是对你应用自定义上下文对象的包装。实践中最常用的成员如下:
wrapper.context:你自己的可变应用状态与依赖(唯一由你定义的对象);wrapper.usage:当前运行累计的请求数与 token 用量。对应源码 usage.py 中的Usage类(字段为requests、input_tokens、output_tokens、total_tokens),通过Usage.add在各请求间累加(见 usage.py)。注意:流式响应下该值在流的最后一个 chunk 处理完成前可能是过期(stale)的;wrapper.tool_input:当当前运行处于Agent.as_tool()内部时,其结构化输入;wrapper.approve_tool(...)/wrapper.reject_tool(...):当需要在代码中程序化更新批准状态时使用(对应 run_context.py 的实现,支持always_approve/always_reject粘性决策)。
牢记:只有wrapper.context是你应用定义的对象,其余字段都是 SDK 管理的运行时元数据。
序列化RunState时的注意事项
如果你后续要为 human-in-the-loop 或持久化任务工作流序列化RunState,这些运行时元数据(usage、approvals、tool_invocations 等)会随状态一起保存。因此,如果你打算持久化或传输序列化后的状态,不要把机密(secrets)放进RunContextWrapper.context——否则机密会跟着状态被写盘或外发。
会话状态是另一回事
会话状态(conversation state)与上面的上下文是两个不同的问题。要根据你想如何跨轮次携带消息来决定使用result.to_input_list()、session、conversation_id还是previous_response_id。相关决策可参考执行结果、Agent 的运行与会话。
完整示例:把用户信息注入工具
以下代码是文档中的标准示例(可直接运行),演示了上下文对象的创建、传递与读取:
import asyncio from dataclasses import dataclass from agents import Agent, RunContextWrapper, Runner from agents.decorators import tool @dataclass class UserInfo: # (1)! name: str uid: int @tool async def fetch_user_age(wrapper: RunContextWrapper[UserInfo]) -> str: # (2)! """Fetch the age of the user. Call this function to get user's age information.""" return f"The user {wrapper.context.name} is 47 years old" async def main(): user_info = UserInfo(name="John", uid=123) agent = AgentUserInfo! name="Assistant", tools=[fetch_user_age], ) result = await Runner.run( # (4)! starting_agent=agent, input="What is the age of the user?", context=user_info, ) print(result.final_output) # (5)! # The user John is 47 years old. if __name__ == "__main__": asyncio.run(main())逐步拆解:
- (1) 上下文对象:这里用的是 dataclass,你也可以换成 Pydantic 模型或任意类型;
- (2) 工具函数:工具的第一个参数是
RunContextWrapper[UserInfo],实现体内通过wrapper.context读取上下文中的值——注意 LLM 永远看不到这个对象,工具只是“代表”模型去访问本地数据; - (3) 泛型 Agent:用
Agent[UserInfo]声明上下文类型,让类型检查器能捕获错误(例如传入一个接收不同上下文类型的工具); - (4) 传入运行:上下文作为
context=参数传给Runner.run; - (5) 结果:Agent 正确调用工具并取回年龄。
进阶:ToolContext——获取工具级元数据
在某些场景下,你需要访问当前正在执行的工具的额外元数据(名称、调用 ID、原始参数字符串等)。此时使用继承自RunContextWrapper的ToolContext类。
从源码看,ToolContext 直接class ToolContext(RunContextWrapper[TContext]),并且其tool_name、tool_call_id、tool_arguments三个字段带强制校验(tool_context.py),确保运行时一定有值。另外它还暴露tool_call(原始ResponseFunctionToolCall对象)、agent(当前 Agent)与run_config等增强信息。
示例:带调试元数据的天气工具
from typing import Annotated from pydantic import BaseModel, Field from agents import Agent from agents.decorators import tool from agents.tool_context import ToolContext class WeatherContext(BaseModel): user_id: str class Weather(BaseModel): city: str = Field(description="The city name") temperature_range: str = Field(description="The temperature range in Celsius") conditions: str = Field(description="The weather conditions") @tool def get_weather(ctx: ToolContext[WeatherContext], city: Annotated[str, "The city to get the weather for"]) -> Weather: print(f"[debug] Tool context: (name: {ctx.tool_name}, call_id: {ctx.tool_call_id}, args: {ctx.tool_arguments})") return Weather(city=city, temperature_range="14-20C", conditions="Sunny with wind.") agent = Agent( name="Weather Agent", instructions="You are a helpful agent that can tell the weather of a given city.", tools=[get_weather], )ToolContext的字段清单
ToolContext提供与RunContextWrapper相同的.context属性,并额外提供当前工具调用特有的字段:
tool_name:被调用的工具名;tool_call_id:本次工具调用的唯一标识符;tool_arguments:传给工具的原始参数字符串(raw arguments string);tool_namespace:当工具通过tool_namespace()或其他带命名空间的接口加载时,该工具调用的 Responses 命名空间;qualified_tool_name:存在命名空间时,用命名空间限定后的工具名(源码实现见 tool_context.py,调用了tool_trace_name)。
何时用ToolContext,何时用RunContextWrapper
- 需要在执行期间访问工具级元数据→ 用
ToolContext; - 只是想在 Agent 与工具之间共享通用上下文→
RunContextWrapper已足够; - 由于
ToolContext继承自RunContextWrapper,当嵌套的Agent.as_tool()运行提供了结构化输入时,它同样能暴露.tool_input。
Agent / LLM 上下文:把数据送入模型视野
当 LLM 被调用时,它能看到的唯一数据来自对话历史(conversation history)。因此,要想让模型利用某些新数据,就必须以某种方式把这些数据放进历史中。框架提供了四种标准方式:
1. 写入 Agent 的instructions(系统提示 / developer message)
instructions即“系统提示(system prompt)”或“developer message”。它可以是静态字符串,也可以是接收上下文并输出字符串的动态函数——这是让“始终有用的信息”(例如用户名、当前日期)进入模型视野的常见手法。
仓库中提供了现成示例 examples/basic/dynamic_system_prompt.py,演示了动态函数型 instructions 的写法。这种方式的信息在对话开始时就会出现在历史中,且位于“指令链(chain of command)”的较高层级。
2. 追加到Runner.run的input
这与方式 1 类似,但允许你放入指令优先级较低的消息(对应 OpenAI 模型规范中「chain of command」的层级,见 OpenAI Model Spec 相关章节)。适用于那些应当被模型参考、但不应压过系统指令的内容。
3. 通过FunctionTool实例暴露——按需(on-demand)上下文
这是on-demand(按需)上下文的最佳实践:LLM 自己判断“我现在需要这份数据”,然后调用对应的工具去获取。数据不在每次请求中全量注入,而是“用到才取”,从而节省 token 并保证数据新鲜。fetch_user_age示例本质上就是这种方式——模型决定调用工具,工具从wrapper.context读取数据并返回。
4. 检索(retrieval)或 Web 搜索(web search)
这些是能够从文件/数据库检索相关数据(retrieval)或从 Web 获取数据(web search)的特殊工具。当你希望模型把回答“基于(grounding)”在相关的上下文数据上时非常有用。仓库的 examples/tools 目录下提供了web_search.py、file_search.py等可直接运行的示例。
四种方式的选型建议
| 方式 | 信息性质 | 注入时机 | 典型场景 |
|---|---|---|---|
instructions(静态/动态函数) | 始终有用、变化低频 | 每轮对话开始时 | 用户名、当前日期、全局规则 |
input追加消息 | 单轮相关、优先级低于指令 | 本次运行 | 一次性任务背景材料 |
FunctionTool | 按需获取、可动态变化 | 模型决定调用时 | 数据库查询、API 拉取、用户画像 |
| 检索 / Web 搜索 | 外部数据、需要 grounding | 模型决定调用时 | RAG、事实核查、实时信息 |
总结与最佳实践清单
- 两种上下文别混淆:
RunContextWrapper.context是给你的代码用的本地对象,绝不会发给 LLM;LLM 只能看到对话历史里的内容; - 同类型约束:一次运行内所有 Agent、工具、钩子必须使用同一类型的上下文,配合
Agent[T]泛型让类型检查器把关; - 共享语义:单次运行内派生 wrapper 共享应用上下文、批准状态与用量;嵌套
Agent.as_tool()不会默认复制应用状态; - 能力可见性:函数工具、MCP 工具、handoff 的启用回调都接收运行上下文,把共享策略放进上下文统一适配;但这些回调不能做授权,授权要放在工具实现、工具输入护栏、批准机制或 MCP 服务器内部;
- 机密管理:要序列化
RunState就别把 secrets 放进context; - 喂给 LLM 的四种姿势:动态
instructions、input消息、按需FunctionTool、检索/Web 搜索,按信息性质与更新频率选择。
如需进一步深入,可继续阅读仓库中的运行上下文源码、工具上下文源码,以及 guardrails、human_in_the_loop、handoffs、sessions 等关联文档,构建完整的上下文与运行状态知识体系。
【免费下载链接】openai-agents-pythonA lightweight, powerful framework for multi-agent workflows项目地址: https://gitcode.com/GitHub_Trending/op/openai-agents-python
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考