
从「`print(result.final_output)` 输出 None」和「直接把 result 传给下一轮导致对话污染」两个高频坑切入,系统拆解 `RunResultBase` → `RunResult` → `RunResultStreaming` 的三层继承结构、`final_output` 三态逻辑、`new_items` 13 种 RunItem 子类型、`to_input_list()` 两种模式、`last_agent` 动态路由、Guardrail 结果数组,以及流式场景的必消费约定与 `cancel()` 两种模式,附 3 条可落地实践建议。
Runner.run() 一切正常,没有报错。然后你写下这行代码:print(result.final_output)None。result 传给下一轮 Runner.run(),以为多轮对话就这样接上了。结果 Agent 表现越来越奇怪,好像对话历史被污染了一样。RunResult 到底是什么,大多数人从来没认真看过。| 类名 | 来源 | 特有字段 |
|---|---|---|
RunResultBase | 抽象基类 | 所有共享属性(9 个) |
RunResult | run() / run_sync() 返回 | max_turns、interruptions、to_state() |
RunResultStreaming | run_streamed() 返回 | current_agent、is_complete、stream_events()、cancel() 等 |
RunResultBase 是核心。它定义了以下 9 个共享属性:input:本次运行的原始输入(str 或 list[TResponseInputItem])new_items:运行过程中产生的所有事件列表(list[RunItem])raw_responses:模型返回的原始响应列表(list[ModelResponse])final_output:最终输出,类型为 Anyinput_guardrail_results、output_guardrail_results:输入/输出级 Guardrail 结果tool_input_guardrail_results、tool_output_guardrail_results:工具调用的 Guardrail 结果context_wrapper:运行上下文封装(RunContextWrapper[Any])
final_output 的三种状态str 类型last_agent 没有定义 output_type 时,最终输出是纯文本字符串。大多数基础场景属于这种。last_agent 定义了 output_type(通常是 Pydantic 模型),输出是该类型的实例。由于 handoff 可能在运行中切换 Agent,SDK 无法静态推断输出类型,所以类型标注是 Any——需要你自己处理类型转换。final_output_as() 方法简化这件事1:# raise_if_incorrect_type=True 时非目标类型会抛 TypeError
output = result.final_output_as(MyOutputModel, raise_if_incorrect_type=True)Nonefinal_output 为 None:RunResultStreaming 的 final_output 在流结束前始终为 Nonecancel(mode='immediate') 后 Agent 没能完成最后一轮result = await runner.run(agent, input="请帮我分析这份数据")
if result.final_output is None:
# 检查是否有中断项待审批
if result.interruptions:
print(f"运行暂停,等待审批: {result.interruptions}")
else:
print("运行未完成")
else:
print(result.final_output)new_items:13 种 RunItem 子类型| 类型 | 含义 |
|---|---|
MessageOutputItem | Agent 产生的文本消息 |
ToolCallItem | 工具调用请求 |
ToolCallOutputItem | 工具调用返回结果 |
HandoffCallItem | Handoff 切换请求 |
HandoffOutputItem | Handoff 切换完成 |
ReasoningItem | 推理过程(启用了 reasoning 时) |
ToolSearchCallItem | 搜索工具调用请求 |
ToolSearchOutputItem | 搜索工具调用返回 |
MCPListToolsItem | MCP 工具列表请求 |
MCPApprovalRequestItem | MCP 审批请求 |
MCPApprovalResponseItem | MCP 审批响应 |
CompactionItem | 上下文压缩事件 |
ToolApprovalItem | 需要人工审批的工具调用 |
from agents.items import (
MessageOutputItem,
ToolCallItem,
ToolCallOutputItem,
HandoffCallItem,
)
# 提取 Agent 所有文本输出
messages = [
item for item in result.new_items
if isinstance(item, MessageOutputItem)
]
# 提取所有工具调用(便于日志审计)
tool_calls = [
(item.tool_name, item.tool_input)
for item in result.new_items
if isinstance(item, ToolCallItem)
]
# 检查是否发生了 Handoff
handoffs = [
item for item in result.new_items
if isinstance(item, HandoffCallItem)
]
if handoffs:
print(f"发生了 {len(handoffs)} 次 Agent 切换")to_input_list() 的正确用法to_input_list() 是多轮对话的核心接口,也是最容易用错的地方。preserve_all(默认):将 new_items 转换为完整的 input-item 历史,原样保留所有事件normalized:在 handoff 过滤或重写模型历史时使用规范化接续输入;对于没有发生 handoff 的普通运行,行为与 preserve_all 一致# ✅ 正确:用 to_input_list() 构建下一轮的 input
input_messages = result.to_input_list()
input_messages.append({"role": "user", "content": "继续分析第二个维度"})
result2 = await runner.run(agent, input=input_messages)
# ❌ 错误:直接把 result 传给下一轮
result2 = await runner.run(agent, input=result) # 这会报类型错误
# ❌ 错误:只传 final_output,丢失了工具调用历史
result2 = await runner.run(agent, input=result.final_output)| 策略 | 状态存于 | 适用场景 |
|---|---|---|
to_input_list() | 应用内存 | 简单多轮、需要自定义历史管理 |
Session | 存储后端 + SDK | 生产环境持久化对话(上期 #13 详解) |
conversation_id | OpenAI Conversations API | 云端托管,客户端轻量 |
previous_response_id | OpenAI server-side | 最轻量的服务端续行 |
last_agent 与多 Agent 场景last_agent 指向「最后一个真正跑完的接手方」2——用它来决定下一步找谁对话,比维护一个全局状态变量干净多了。result = await runner.run(triage_agent, input=user_message)
# 根据 last_agent 决定下一步
if result.last_agent.name == "billing_agent":
# 账单问题,继续追问细节
followup = "你的账单问题已初步处理,需要开具发票吗?"
elif result.last_agent.name == "tech_support_agent":
followup = "技术问题处理中,请描述报错信息"
else:
followup = "您的问题已收到,稍后跟进"
next_input = result.to_input_list() + [{"role": "user", "content": followup}]| 属性 | 触发时机 |
|---|---|
input_guardrail_results | 输入消息进入 Agent 前 |
output_guardrail_results | Agent 产出最终输出后 |
tool_input_guardrail_results | 工具被调用前 |
tool_output_guardrail_results | 工具返回结果后 |
None。有触发时,元素按配置顺序排列:# 检查是否有 Guardrail 被触发
for gr in result.input_guardrail_results:
if gr.output.tripwire_triggered:
print(f"输入 Guardrail 触发: {gr.guardrail.__class__.__name__}")
print(f" 原因: {gr.output.output_info}")
# 工具级别 Guardrail 审计
for gr in result.tool_output_guardrail_results:
print(f"工具: {gr.guardrail.__class__.__name__}, "
f"触发: {gr.output.tripwire_triggered}")Runner.run() 调用期间所有 Guardrail 的检查记录都在里面。排查「运行为什么突然中断」时,第一个该看的地方就是这里。current_agent:当前正在运行的 Agent(动态更新)current_turn:当前轮次编号is_complete:流是否已终止(bool)run_loop_exception:后台异常容器,用于消费流后检查静默失败stream_events():异步迭代器,消费语义化流事件cancel(mode):取消流运行stream_events() 的必消费约定final_output、interruptions、raw_responses的赋值,以及 Session 持久化等副作用,可能在最后一个可见 token 到达之后仍在后台处理中。
result = runner.run_streamed(agent, input="请生成一份分析报告")
# ✅ 正确:消费完整个迭代器
async with result.stream_events() as events:
async for event in events:
if event.type == "raw_response_event":
# 处理流式 token
pass
# 到这里 is_complete 才可靠为 True
print(result.final_output) # 此时才有值
# ❌ 错误:提前 break 跳出,final_output 可能仍为 None
async for event in result.stream_events():
if some_condition:
break # 后台任务未完成
print(result.final_output) # 不可靠cancel() 的两种模式# immediate:立即停止,取消所有后台任务,清空队列
# 代价:对话历史可能不完整
result.cancel(mode="immediate")
# after_turn:完成当前轮次后停止
# 好处:LLM 响应完成、工具调用结束、Session 状态正确保存
result.cancel(mode="after_turn")
# 注意:调用 cancel() 后仍需继续消费 stream_events()
# 才能让取消和清理操作正常完成
async for event in result.stream_events():
pass # 继续消费,直到迭代器自然终止immediate;业务层面需要保证数据一致性选 after_turn。final_output=None,不要让它崩溃你的代码Runner.run() 调用,强制检查三种状态:from agents import RunResult
def extract_output(result: RunResult, expected_type=None):
if result.final_output is None:
if hasattr(result, "interruptions") and result.interruptions:
raise RuntimeError("运行被 HITL 中断,需要人工审批后恢复")
raise RuntimeError("运行未产生输出(可能被取消或中途出错)")
if expected_type is not None:
return result.final_output_as(expected_type, raise_if_incorrect_type=True)
return result.final_outputnew_items 类型过滤要用 isinstance,不要靠字段猜isinstance 精确匹配类型,然后根据具体子类的 API 访问字段。IDE 补全也会更友好。run()):任务型、批处理、代码驱动的确定性流水线——你只关心最终结果,不需要实时反馈run_streamed()):面向用户的交互界面——需要逐 token 显示让用户感受响应速度;或者运行时间长、需要展示进度RunResult 不是单纯的「结果容器」。它是 SDK 把整个 Agent 运行过程的完整状态——事件流、Guardrail 记录、对话历史接续入口——打包在一起的对象。final_output 的三态、new_items 的 13 种子类型、to_input_list() 的两种模式,加上流式场景的消费约定——把这四块弄清楚,大部分生产代码里遇到的问题都能定位。openai.github.io
openai.github.io
openai.github.io
openai.github.io
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.