OpenAI Agents SDK #32:RunConfig 完整拆解——20+ 字段五大功能域,一次性掌握全局运行配置

OpenAI Agents SDK #32:RunConfig 完整拆解——20+ 字段五大功能域,一次性掌握全局运行配置

RunConfig 是 SDK 里最被低估的对象——20 多个字段涵盖五大功能域。本期完整拆解工具并发限流(ToolExecutionConfig.max_function_tool_concurrency)、模型调用前拦截(call_model_input_filter)、错误优雅降级(error_handlers)、reasoning item ID 策略(omit 防 400)和全局可观测性配置,附生产级配置模板与四条落地建议。

OpenAI Agents SDK 每日技术拆解
2026/5/23 · 9:04
購読 2 件 · コンテンツ 32 件

リサーチノート

你用 Runner.run() 跑 Agent,但有没有想过:要限制工具并发、在模型调用前偷偷改一下输入、让 max_turns 到了不报错而是优雅降级——这些都该怎么做?
答案是同一个地方:RunConfig
RunConfig 是 SDK 里最被低估的对象。它不管 Agent 的逻辑,只管「这次运行怎么跑」。20 多个字段涵盖五大功能域:工具执行、模型输入拦截、错误处理、推理项策略、可观测性与会话设置。本期逐域拆解,重点放在几个最新加入却最少被讲到的字段。
リンクプレビューを読み込んでいます…

一、怎么用 RunConfig

所有 Runner 方法都接受 run_config 参数:
from agents import Agent, RunConfig, Runner

result = await Runner.run(
    agent,
    "请执行任务",
    run_config=RunConfig(
        workflow_name="my-workflow",
        tracing_disabled=False,
    )
)
不传 run_config 时,Runner 自动使用全默认值。传了就覆盖对应字段,不影响其他字段。RunConfig 是 dataclass,所有字段默认 None(布尔字段有明确默认值),只需填你关心的。

二、工具执行控制

ToolExecutionConfig:并发限流

这是 v0.16.0 新增的字段,但几乎没有中文文章提过它。
from agents import Agent, RunConfig, Runner, ToolExecutionConfig

result = await Runner.run(
    agent,
    "同时调用多个工具",
    run_config=RunConfig(
        tool_execution=ToolExecutionConfig(max_function_tool_concurrency=2),
    ),
)
max_function_tool_concurrency:整数或 None。默认 None——模型一轮里发出几个 function tool call,SDK 就同时跑几个。设成 2 就意味着最多同时运行 2 个本地函数工具,其余排队等待。
它和 ModelSettings.parallel_tool_calls 是两回事,必须分清楚:
配置管什么作用层
parallel_tool_calls模型是否允许在单次响应里发出多个 tool call模型侧(请求参数)
max_function_tool_concurrencySDK 接到多个 tool call 后,同时执行几个SDK 侧(运行时)
两者正交。你可以让模型发出 10 个并行 tool call,但 SDK 一次只跑 3 个——适合调用有速率限制的第三方 API、或不希望并发写入同一数据库的场景。

tool_error_formatter:改写拒绝消息

审批流(Human-in-the-loop)里,人工拒绝一个工具调用时,SDK 默认给模型发一条固定错误信息。tool_error_formatter 可以改写这条消息:
from agents import RunConfig, ToolErrorFormatterArgs

def format_rejection(args: ToolErrorFormatterArgs[None]) -> str | None:
    if args.kind == "approval_rejected":
        return (
            f"工具调用 '{args.tool_name}' 被人工拒绝。"
            "请提出一个更安全的替代方案,或请求澄清。"
        )
    return None  # 返回 None 用 SDK 默认消息

result = Runner.run_sync(
    agent,
    "请删除生产数据库",
    run_config=RunConfig(tool_error_formatter=format_rejection),
)
ToolErrorFormatterArgs 包含 6 个字段:
字段类型含义
kindLiteral["approval_rejected"]当前只有这一种错误类型
tool_typeLiteral["function", "computer", "shell", "apply_patch", "custom"]工具运行时类型
tool_namestr工具名称
call_idstr本次 tool call 的唯一 ID
default_messagestrSDK 默认会发给模型的消息
run_contextRunContextWrapper[TContext]当前运行上下文,可读取自定义数据
返回字符串就替换消息,返回 None 就沿用默认。实际上 kind 目前只有 "approval_rejected" 一种,但结构设计留了扩展空间。

三、模型调用前拦截

call_model_input_filter:最后一道关卡

每次 LLM 调用前,SDK 已经把当前轮的输入、历史、Session 内容合并成一个列表准备发送。call_model_input_filter 让你在这个列表真正发出去前,做最后一次干预。1
from agents import Agent, RunConfig, Runner
from agents.run import CallModelData, ModelInputData

def drop_old_messages(data: CallModelData[None]) -> ModelInputData:
    # 只保留最近 5 条输入,防止 context 过长
    trimmed = data.model_data.input[-5:]
    return ModelInputData(input=trimmed, instructions=data.model_data.instructions)

result = Runner.run_sync(
    agent,
    "解释一下递归",
    run_config=RunConfig(call_model_input_filter=drop_old_messages),
)
回调收到 CallModelData,必须返回 ModelInputData,它有两个字段:
  • inputlist[TResponseInputItem],必填,发给模型的输入列表
  • instructionsstr | None,可选,System prompt 内容
三个重要的时序细节:
  1. 如果用了 Session,call_model_input_filter 在 Session 历史已经加载并合并之后运行——你拿到的已经是完整的「历史 + 当前输入」列表
  2. 如果用了服务端会话(conversation_id / previous_response_id),filter 收到的是下一次 Responses API 请求的 payload,可能只是增量 delta,不是完整历史回放
  3. Runner 把输入列表的副本传给 filter,直接修改不会影响调用方原始列表
典型用途:按 token 预算裁剪历史、在每次调用前注入动态系统提示、屏蔽敏感字段。
リンクプレビューを読み込んでいます…

session_input_callback:更早的会话合并拦截

call_model_input_filter 管的是「合并之后」的最终输入列表。如果你想自定义「新输入怎么和历史合并」这个过程本身,用 session_input_callback——它在合并阶段执行,早于 call_model_input_filter。两者不冲突,可以同时配置。

四、错误处理器

error_handlers:让 MaxTurns 不再抛异常

默认情况下,超过 max_turns 会抛 MaxTurnsExceeded;模型拒绝输出则抛 ModelRefusalErrorerror_handlers 让你把这两种情况转成受控输出,而不是异常。
from agents import (
    Agent,
    RunErrorHandlerInput,
    RunErrorHandlerResult,
    Runner,
)

def on_max_turns(data: RunErrorHandlerInput[None]) -> RunErrorHandlerResult:
    return RunErrorHandlerResult(
        final_output="我在轮数限制内没能完成,请缩小请求范围。",
        include_in_history=False,  # 这条降级输出不追加到对话历史
    )

result = Runner.run_sync(
    agent,
    "分析这份超长文档",
    max_turns=3,
    error_handlers={"max_turns": on_max_turns},
)
print(result.final_output)
error_handlers 是一个字典,当前支持两个 key:"max_turns""model_refusal"
model_refusal 的用法更有意思——可以配合 structured output 做兜底:
from pydantic import BaseModel
from agents import Agent, ModelRefusalError, RunErrorHandlerInput, Runner

class Recipe(BaseModel):
    ingredients: list[str]
    refusal_reason: str | None = None

def on_model_refusal(data: RunErrorHandlerInput[None]) -> Recipe:
    assert isinstance(data.error, ModelRefusalError)
    return Recipe(ingredients=[], refusal_reason=data.error.refusal)

agent = Agent(
    name="Recipe assistant",
    instructions="返回一份结构化菜谱。",
    output_type=Recipe,
)
result = Runner.run_sync(
    agent,
    "做一道危险的东西。",
    error_handlers={"model_refusal": on_model_refusal},
)
# 不抛异常,拿到 Recipe(ingredients=[], refusal_reason="...")
include_in_history=False 很关键:如果是降级消息,你通常不希望它出现在下一轮的历史里,避免污染后续对话。

五、推理项 ID 策略

reasoning_item_id_policy:o 系列模型的 400 错误克星

这是一个比较冷门但很实用的字段,专门解决 o1o3 等带 reasoning 输出的模型在多轮对话中触发的 400 错误。
错误长这样:
Item 'rs_...' of type 'reasoning' was provided without its required following item.
根本原因:SDK 在多轮 Agent 运行时,会把上一轮的输出(包括 reasoning items)组织成下一轮的输入列表。Responses API 要求:如果你在请求中带上了某个 reasoning item 的 id,那个 id 对应的后续 item 也必须跟着传——但在某些场景下(session 持久化、streamed 后跟 non-streamed、handoff 后的 resume 等),SDK 构建的列表里可能只有 reasoning item 本身,没有配套的 following item。
解决方案就是丢掉 reasoning item 的 id,只保留内容:
result = await Runner.run(
    agent,
    "一个需要推理的复杂问题",
    run_config=RunConfig(reasoning_item_id_policy="omit"),
)
行为
None"preserve"(默认)保留 reasoning item ID
"omit"去掉 reasoning item ID,只保留内容
注意范围:这只影响 SDK 自动构建的后续输入列表,不影响你手动传入的初始输入。如果你之后用 call_model_input_filter 重新注入了 reasoning ID,那照样会带过去。
リンクプレビューを読み込んでいます…

六、字段全览

模型与 Provider(5 字段)

字段作用常用场景
model覆盖所有 Agent 的模型统一切换模型版本,不改 Agent 定义
model_provider模型名称解析器(默认 OpenAI MultiProvider)接第三方 LLM 时替换
model_settings全局覆盖 temperature / top_p 等A/B 测试、批量生产时统一参数
session_settings覆盖 Session 的默认设置(如历史条数限制)控制 Session 检索窗口
session_input_callback自定义新输入与会话历史的合并方式滑动窗口、优先级排序

Guardrails 与 Handoff(4 字段)

字段作用
input_guardrails对所有运行的初始输入追加全局输入护栏
output_guardrails对所有运行的最终输出追加全局输出护栏
handoff_input_filter所有 handoff 的全局输入过滤器(被单个 Handoff.input_filter 覆盖)
nest_handoff_history是否把 handoff 前的历史折叠成单条助手消息(opt-in beta,v0.15.0+,v0.17.1 修复历史丢失 bug)
handoff_history_mapper自定义折叠历史的函数(nest_handoff_history=True 时生效)

可观测性(6 字段)

字段作用
tracing_disabled关闭本次运行的 trace
tracingTracingConfig:覆盖 trace export 设置(如用不同 API key)
trace_include_sensitive_data是否把 LLM 输入输出写入 trace(默认行为受全局设置影响)
workflow_nametrace 里的工作流名(默认 "Agent workflow",强烈建议设置)
trace_id自定义 trace ID
group_id链接同一对话多次运行的 trace,填对话 thread ID
trace_metadatatrace 里附带的额外 key-value 元数据

七、实战配置模板

生产服务标准配置:
run_config = RunConfig(
    workflow_name="customer-support-v2",
    group_id=session_id,              # 把同一用户会话的多次 run 串成一组 trace
    tool_execution=ToolExecutionConfig(
        max_function_tool_concurrency=3,  # 限制第三方 API 并发
    ),
    call_model_input_filter=trim_history_to_budget,  # 按 token 预算裁剪历史
    error_handlers={
        "max_turns": graceful_max_turns_fallback,    # 优雅降级
        "model_refusal": structured_refusal_handler, # 结构化拒绝消息
    },
    reasoning_item_id_policy="omit",  # 如果用 o 系列模型,防 400 错误
)
调试模式配置(关闭 trace 敏感数据):
run_config = RunConfig(
    workflow_name="debug-session",
    trace_include_sensitive_data=False,  # 不把 LLM 输入输出写进 trace
    tracing_disabled=False,              # trace 仍然开着,只是不带敏感内容
)

落地建议

  1. workflow_name 必填。生产里跑了几十个不同 Agent 流,如果全都是默认的 "Agent workflow",trace 面板里根本没法区分。一行代码,trace 可读性直接上一个档次。
  2. 限制 tool 并发之前先量化max_function_tool_concurrency 设多少要结合你调用的 API 的速率限制来算,不是越低越好——限太死会把并行化优势全部消掉。先跑压测,看第三方服务的 429 频率,再决定值。
  3. reasoning_item_id_policy="omit"o 系列模型的标配。只要你的 Agent 会跑多轮(有 handoff / session / 历史管理),就默认加上,能省掉很多奇怪的 400 排查时间。它只影响 SDK 自动构建的 follow-up input,副作用极低。
  4. call_model_input_filter 不要替代 Session 机制。它适合做最后一道 token 预算卡关,或在调用前注入动态系统上下文。如果你想管的是「历史怎么存怎么取」,那是 Session + session_input_callback 的活。
21

このコンテンツについて、さらに観点や背景を補足しましょう。

  • ログインするとコメントできます。