AgentOps OpenAI Agents SDK 测试夹具生成器:从真实 API 响应到可复用的单元测试数据
2026/9/17 23:34:53 网站建设 项目流程

AgentOps OpenAI Agents SDK 测试夹具生成器:从真实 API 响应到可复用的单元测试数据

【免费下载链接】agentopsPython SDK for AI agent monitoring, LLM cost tracking, benchmarking, and more. Integrates with most LLMs and agent frameworks including CrewAI, Agno, OpenAI Agents SDK, Langchain, Autogen, AG2, and CamelAI项目地址: https://gitcode.com/GitHub_Trending/ag/agentops

AgentOps 对 OpenAI Agents SDK 的自动埋点(instrumentation)依赖真实的 API 响应结构来做单元测试。本篇技术指南围绕 tests/unit/instrumentation/openai_agents/tools/README.md 展开,讲解该仓库配套的“测试夹具生成器”:如何通过真实的 OpenAI Agents API 调用一键生成标准响应与工具调用响应两类 JSON 夹具,并深入 generate_fixtures.py 的实现细节,说明这些夹具如何被 test_openai_agents.py 与 test_openai_agents_attributes.py 消费,从而离线、可复现地验证 AgentOps 的 span 属性提取与 token 用量统计逻辑。读完本篇,你将能够独立运行该生成器、理解夹具的字段结构,并知道何时、为什么需要重新生成夹具。

一、定位与背景:为什么需要“真实响应”夹具

AgentOps 的 OpenAI Agents SDK 埋点模块(agentops/instrumentation/agentic/openai_agents/)通过拦截 SDK 的 trace processor 接口和 monkey-patchRunner,将 Agent、Function、Generation 等 span 数据转换为 OpenTelemetry span 与指标,模块设计详见 agentops/instrumentation/agentic/openai_agents/README.md。

单元测试要验证的核心问题是:埋点代码能否正确解析 SDK 返回的对象结构。Agents SDK 的RunResult内部包装了原始 API 响应(raw_responses),其中可能包含 Chat Completions 或 Response API 两种不同格式。若测试全部使用手工构造的 mock 数据,一旦 SDK 或 API 结构演进,测试就会失真。因此仓库提供了这个“dead simple”的开发脚本(README 原话),直接调用真实 API 抓取响应并落盘为 JSON:

  • 发起两类 OpenAI Agents API 调用:标准 Agent 响应、带工具调用(tool calls)的 Agent 响应;
  • 将 JSON 响应保存到../fixtures/(相对脚本位置,即tests/unit/instrumentation/fixtures/);
  • 仅此而已——这是一个刻意保持简单的一次性开发工具,脚本自述“Dev tool only - no frills, just gets the job done”。

二、运行方式与环境要求

README 给出的运行步骤如下:

# 激活虚拟环境 source .venv/bin/activate # 运行生成器 python -m tests.unit.instrumentation.openai_agents_tools.generate_fixtures

需要注意一个仓库演进带来的路径差异:脚本当前实际位于 tests/unit/instrumentation/openai_agents/tools/generate_fixtures.py,且该目录下存在init.py 使其成为合法包,__init__.py中的模块说明为“OpenAI Agents Tools for AgentOps instrumentation”。因此按当前目录结构,可执行的等价模块路径是:

python -m tests.unit.instrumentation.openai_agents.tools.generate_fixtures

两种写法对应同一条执行入口:脚本末尾通过asyncio.run(main())启动异步主流程(见 generate_fixtures.py)。

依赖与前提

README 声明的前置条件有两条,结合 pyproject.toml 可以落实为可操作的环境清单:

  1. OpenAI API Key:通过环境变量或.env文件提供。脚本在导入阶段调用load_dotenv()(第 23 行)加载.env,之后由agentsSDK 在发起请求时读取OPENAI_API_KEY
  2. 已安装openaiopenai-agents。在pyproject.tomltest依赖组中可以看到对应声明:openai>=1.60.0openai-agents[voice](第 57、69 行)。由于[tool.uv]配置了default-groups = ["test", "dev"],用 uv 同步环境时这些包会被一并安装。

另外,python-dotenv位于dev依赖组(第 87 行),保证load_dotenv()可用。整个运行过程会真实消耗少量 OpenAI API 调用(两次Runner.run),这是该工具与纯离线测试工具的本质区别。

三、脚本实现剖析:generate_fixtures.py

脚本共 150 行,结构清晰,可拆为四个部分。

3.1 输出路径与常量

# Output paths FIXTURES_DIR = "../fixtures" # Relative to this script's location AGENT_RESPONSE_FILE = "openai_agents_response.json" AGENT_TOOL_RESPONSE_FILE = "openai_agents_tool_response.json" def get_fixtures_dir(): """Get absolute path to fixtures directory""" return os.path.join(os.path.dirname(os.path.abspath(__file__)), FIXTURES_DIR)

get_fixtures_dir()os.path.abspath(__file__)锚定脚本自身位置,再把../fixtures解析为绝对路径——即tests/unit/instrumentation/fixtures/。这与 README 中“保存到../fixtures/”的描述一致。main()会先打印该目录并os.makedirs(..., exist_ok=True),目录不存在时自动创建。

值得注意的是,这个夹具目录是多个 provider 测试共享的:同目录下还有anthropic_message.jsonopenai_chat_completion.jsonopenai_response.json等文件,以及同类生成器 generate_anthropic_fixtures.py。

3.2 通用序列化:model_to_dict

def model_to_dict(obj: Any) -> Dict: """Convert an object to a dictionary, handling nested objects.""" if obj is None: return None if isinstance(obj, (str, int, float, bool)): return obj if isinstance(obj, (list, tuple)): return [model_to_dict(item) for item in obj] if isinstance(obj, dict): return {key: model_to_dict(value) for key, value in obj.items()} # 其他对象:遍历非下划线、非可调用属性递归转换 result = {} for key in dir(obj): if not key.startswith("_") and not callable(getattr(obj, key)): try: value = getattr(obj, key) result[key] = model_to_dict(value) except Exception as e: result[key] = f"<Error: {e}>" return result

RunResult不是原生 dict,无法直接json.dumpmodel_to_dict采用“递归反射”策略:基础类型原样返回,list/tuple/dict 递归展开,任意 SDK 对象则遍历其公共属性(跳过_前缀与可调用项),转换失败时以"<Error: ...>"占位而非中断。配合落盘时的json.dump(..., default=str),确保任意不可序列化字段也有兜底。

3.3 夹具一:标准 Agent 响应

generate_standard_agent_response()(第 59-87 行)定义一个最简 Agent 并执行一次问答:

agent = Agent( name="Fixture Generation Agent", instructions="You are a helpful assistant designed to generate test fixtures. Respond concisely.", ) result = await Runner.run(agent, "What is the capital of France?")

Runner.run返回的RunResultmodel_to_dict转换后,以indent=2写入openai_agents_response.json。异常会被捕获并打印前缀信息、返回{"error": ...},保证两类夹具相互独立——一次失败不会阻断第二次调用。

3.4 夹具二:带工具调用的 Agent 响应

generate_tool_agent_response()(第 90-128 行)在 Agent 上挂载一个function_tool

def get_weather(location: str, unit: str = "celsius") -> str: """Get weather information for a location.""" return f"The weather in {location} is 22 degrees {unit}." weather_tool = function_tool( get_weather, name_override="get_weather", description_override="Get the current weather in a location" ) agent = Agent( name="Tool Fixture Generation Agent", instructions="You are a helpful assistant designed to generate test fixtures. Use tools when appropriate.", tools=[weather_tool], ) result = await Runner.run(agent, "What's the weather in Paris?")

该工具是纯本地的假实现(固定返回 22 度),其价值在于迫使 SDK 走完整“模型决策 → tool_call → 本地执行 → 二次模型调用”链路,从而让夹具里天然带有tool_calls结构。function_toolname_override/description_override参数则显式固定了工具的对外名称与描述,降低夹具内容的随机性。

四、生成夹具的字段结构

两个夹具文件已在仓库中提交,可以直接阅读:

4.1 openai_agents_response.json

{ "final_output": "The capital of France is Paris.", "input": "What is the capital of France?", "raw_responses": [ { "referenceable_id": "resp_67db29270db8819290bc1ef0b7e0cf530eb1154d079a2e67", "output": [ { "id": "msg_67db29277e6c81928cdceaea2b4893f30eb1154d079a2e67", "content": [{ "text": "The capital of France is Paris.", "type": "output_text", "annotations": [] }], "role": "assistant", "status": "completed", "type": "message" } ], "usage": { "input_tokens": 54, "output_tokens": 8, "requests": 1, "total_tokens": 62 } } ] }

4.2 openai_agents_tool_response.json

与标准响应同构,差异在于output[0]中新增了tool_calls数组:

"tool_calls": [ { "id": "call_xyz789", "type": "tool_call", "function": { "name": "get_weather", "arguments": "{\"location\":\"New York City\",\"units\":\"celsius\"}" } } ]

两份夹具共同体现了 Agents SDK 响应格式的三个关键特征(在 test_openai_agents_attributes.py 的头部注释中有完整描述):

  1. 嵌套结构:真正的 API 响应藏在raw_responses数组内,内容路径为raw_responses[0].output[0].content[0].text
  2. token 命名usage使用input_tokens/output_tokens(Response API 命名),而非 Chat Completions 的prompt_tokens/completion_tokens
  3. 工具参数为字符串function.arguments是序列化后的 JSON 字符串而非对象。埋点的序列化规则(见模块 README 的 Serialization Rules)要求“不要解析字符串内的 JSON、保持原始形态”,夹具恰好为这一规则提供了真实样本。

五、下游消费:夹具如何驱动单元测试

5.1 加载方式

tests/unit/instrumentation/openai_agents/test_openai_agents.py 与 test_openai_agents_attributes.py 都实现了同一套load_fixture工具函数:以测试文件所在目录的上一级为基准拼接fixtures/<name>.jsonjson.load,随后在模块导入期一次性加载全部 6 个夹具(4 个 OpenAI 原生格式 + 2 个 Agents SDK 格式)。

5.2 验证点示例

  • 标准响应test_response_api_span_serializationAGENTS_RESPONSE作为output注入GenerationSpanData,经process_with_instrumentorOpenAIAgentsExporter,断言完成内容、角色、token 用量(54/8/62,与夹具一致)和LLM_SYSTEM == "openai"等语义属性;
  • 工具调用响应test_tool_calls_span_serialization验证COMPLETION_TOOL_CALL_IDCOMPLETION_TOOL_CALL_NAME(值为get_weather)、COMPLETION_TOOL_CALL_ARGUMENTS(包含{"location":"New York City","units":"celsius"})三类属性被正确抽取;
  • token 处理test_token_usage_processing_from_fixture直接对AGENTS_RESPONSE["raw_responses"][0]["usage"]调用process_token_usage,断言LLM_USAGE_PROMPT_TOKENS == 54LLM_USAGE_COMPLETION_TOKENS == 8LLM_USAGE_TOTAL_TOKENS == 62——夹具数字与断言严格绑定;
  • 嵌套 usage 提取test_extract_nested_usage_from_fixturesextract_nested_usage(实现位于 agentops/instrumentation/agentic/openai_agents/attributes/tokens.py)分别验证 Chat Completions、Response API、Agents SDK 三种格式的 usage 字段提取。

由于pyproject.toml[tool.pytest.ini_options]配置了testpaths = ["tests/unit"],直接运行pytest即可执行这套不依赖网络、不依赖真实 API Key 的夹具测试(集成测试被--ignore=tests/integration排除)。这正是“生成一次真实夹具、之后永久离线复用”策略的价值。

六、适用前提、限制与最佳实践

结合脚本与配置,使用时应明确以下边界:

  • 需要真实网络与 API Key:每次运行都会发生真实 LLM 调用,会产生少量费用;不适合放进 CI 或无网环境。仓库默认测试路径tests/unit中的测试本身不调用 API,只有手动运行生成器才会。
  • 输出是“快照”而非“契约”:重新运行会覆盖同名 JSON,referenceable_idmsg_*ID 和 token 计数会随每次运行变化(当前提交的工具调用夹具中的 ID 为占位风格的resp_abc123def456/call_xyz789,可见已被人工规整)。重新生成后,需同步核对依赖具体数值的断言(如 54/8/62 token 断言)。
  • 错误被降级而非抛出:任一 API 调用失败只会打印错误并写出{"error": ...}占位,不中断另一项生成;因此运行后应检查产物内容而非仅看退出码。
  • Python 版本pyproject.toml声明requires-python = ">=3.9",OpenTelemetry 依赖按 3.9 与 3.10+ 分别 pin 版本,按仓库约定用 uv 创建 venv 即可满足脚本所需环境。

典型工作流:SDK 升级或 API 结构变化时 → 配置OPENAI_API_KEY→ 运行生成器 → 人工审阅两份新夹具的结构与 ID → 更新受影响的断言 → 用pytest跑通tests/unit全量单测。

小结

tests/unit/instrumentation/openai_agents/tools/README.md 描述的是一个极简但定位精准的开发工具:以两次真实 OpenAI Agents API 调用(标准响应 + 工具调用响应)产出 JSON 夹具,供 AgentOps 的 OpenAI Agents SDK 埋点单元测试离线复用。理解它的关键在于把握三层关系:generate_fixtures.py 的反射式序列化把 SDK 对象快照为稳定结构;openai_agents_response.json 与 openai_agents_tool_response.json 保留了raw_responses嵌套、Response API token 命名、字符串化工具参数三个真实特征;而测试文件则把这些特征逐字段映射到SpanAttributes/MessageAttributes语义约定上做断言。这一“真实快照 + 离线断言”的模式,也是仓库内其他 provider(如 Anthropic)夹具生成器的共同范式。

【免费下载链接】agentopsPython SDK for AI agent monitoring, LLM cost tracking, benchmarking, and more. Integrates with most LLMs and agent frameworks including CrewAI, Agno, OpenAI Agents SDK, Langchain, Autogen, AG2, and CamelAI项目地址: https://gitcode.com/GitHub_Trending/ag/agentops

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询