很多 Agent Demo 到"模型成功调用函数"就结束了。但在生产环境里,真正重要的问题是:它调用了吗?调用对了吗?结果有没有被模型编造或漏掉?
今天我用 OpenAI Agents SDK 完成了一条从最小工具调用到自动化评测的实践链路。过程中不仅遇到了 Python 版本兼容问题,也逐步建立了工具轨迹、参数断言、边界测试和最终答案评测。
如果你正在学习 AI Agent,希望这篇文章能帮你少走一些"Demo 能跑、上线没底"的弯路。
一、为什么 Agent 不能只看最终答案
假设模型回答:
text
你今天有两场会议:
10:00 统一认证方案评审
15:00 记忆模块周会
答案完全正确,能否证明它查询了日历?
不能。
它可能真的调用了工具,也可能碰巧猜对,甚至可能从之前的上下文中看到了答案。
因此需要区分:
text
final_output:最终说了什么
new_items:运行过程中实际发生了什么
这也是我今天最大的认识:结果正确和过程正确是两种不同的质量。
二、最小 Agent:模型、工具和 Runner
我先创建了一个只负责查询日历的 Agent:
python
from agents import Agent, Runner, function_tool
@function_tool
def query_calendar(user_id: str) -> str:
"""查询指定用户今天的日历。
Args:
user_id: 用户 ID,例如 liuxinzhou。
"""
return '[{"time":"10:00","title":"统一认证方案评审"}]'
calendar_agent = Agent(
name="日历助手",
instructions=(
"涉及会议或日程时必须调用 query_calendar,"
"只能根据工具结果回答。"
),
tools=[query_calendar],
)
result = await Runner.run(
calendar_agent,
"请查询用户 liuxinzhou 今天有哪些会议。",
)
print(result.final_output)
可以用一个生活类比理解三者:
Agent是带着岗位说明书的员工;function_tool是员工被授权使用的业务系统;Runner是调度员,负责模型与工具之间的循环。
模型并不直接执行 Python。它提出一个结构化调用请求,Runner 执行函数,再把结果交回模型。
三、类型正确不代表业务正确
工具参数写成:
python
def query_calendar(user_id: str)
str 能帮助 SDK 生成参数 Schema,检查 user_id 是否为字符串。但它只能回答"参数长得对不对",不能回答:
text
这个用户存在吗?
调用者有权查询他吗?
生产系统至少有三层校验:
text
类型校验:参数是不是字符串
业务校验:用户是否存在
权限校验:调用者是否有权查询
这三个概念不能混为一谈。
四、用运行轨迹证明工具真的执行了
Runner.run() 返回的 result 不是 JSON 字典,而是 SDK 的结果对象。在当前项目锁定的版本中,可以读取:
python
result.final_output
result.new_items
一次真实运行出现了如下轨迹:
text
ToolCallItem
→ ToolCallOutputItem
→ MessageOutputItem
含义分别是:
- 模型请求调用工具;
- Python 工具执行并产生结果;
- 模型基于结果生成最终消息。
我没有直接打印整个 SDK 对象,而是提取安全摘要:
python
def summarize_run_items(items):
evidence = []
for item in items:
if item.type == "tool_call_item":
evidence.append({
"event": "tool_called",
"tool": item.raw_item.name,
"arguments": item.raw_item.arguments,
})
elif item.type == "tool_call_output_item":
evidence.append({
"event": "tool_returned",
"output": item.output,
})
return evidence
生产日志需要脱敏。完整对象可能包含系统提示、用户数据或其他敏感上下文。
五、空结果和工具异常不是一回事
我专门测试了不存在的用户 unknown:
text
tool_called: query_calendar({"user_id":"unknown"})
tool_returned: []
final_message: 没有查到今天的会议
这里必须区分三种状态:
| 状态 | 业务含义 | 合理回答 |
|---|---|---|
返回 [] |
查询成功,没有数据 | 没有查到会议 |
| 抛出网络异常 | 查询失败,结果未知 | 服务暂时不可用 |
| 没有调用轨迹 | 根本没有查询 | 不能声称结果来自系统 |
如果把网络异常回答成"没有会议",就是把"未知"错误地表达成"确定没有"。这类错误在审批、库存、订单和财务场景中非常危险。
六、双工具:可以调用、应该调用、确实调用
第二个工具用于查询 Git 提交:
python
@function_tool
def query_git_commits(author: str) -> str:
"""查询指定作者今天的 Git 提交记录。"""
Agent 同时注册两个工具:
python
tools=[query_calendar, query_git_commits]
用户询问"总结今天的会议和代码提交"时,真实轨迹为:
text
query_calendar({"user_id":"liuxinzhou"})
query_git_commits({"author":"liuxinzhou"})
→ 两个工具分别返回
→ 模型生成汇总
这里要分清三个层次:
text
工具已注册 → Agent 可以调用
任务需要工具 → Agent 应该调用
轨迹有调用和返回 → 证明本次确实调用
注册的工具不是越多越好。无关工具会增加误选概率、延迟、费用和权限风险。
七、把人工看日志升级为自动化轨迹评测
如果每次都靠人阅读日志,就无法评测几十或几百个案例。我把正确轨迹写成确定性规则:
python
expected_tools = {
"query_calendar",
"query_git_commits",
}
expected_arguments = {
"query_calendar": {"user_id": "liuxinzhou"},
"query_git_commits": {"author": "liuxinzhou"},
}
评测器自动检查:
- 是否缺少必需工具;
- 是否调用了无关工具;
- 是否重复调用;
- 参数是否为有效 JSON;
- 参数具体值是否正确;
- 每次调用是否都有返回。
为什么参数也必须检查?因为下面的轨迹工具选对了,业务仍然是错的:
text
query_calendar({"user_id":"manager"})
用户要求查询 liuxinzhou,Agent 却查了 manager。
Prompt 与断言的关系可以这样理解:
text
Prompt:告诉模型应该怎么做
断言:检查模型实际上怎么做了
八、轨迹正确,最终答案仍可能错误
即使两个工具都调用正确,模型也可能:
- 漏掉一场会议;
- 把两个会议时间对调;
- 漏掉一个提交;
- 添加不存在的"产品发布会"。
因此还要检查最终答案。
我把工具数据转换为 8 个必须出现的事实:两个会议时间、两个会议标题、两个提交哈希、两个提交说明。
python
missing = [
fact for fact in required_facts
if fact not in answer
]
同时设置已知错误事实:
python
forbidden_facts = [
"产品发布会",
"删除生产数据库",
]
最终形成两类指标:
- 完整性:重要事实是否全部覆盖;
- 忠实性:答案是否有数据依据。
确定性代码能够判断的内容,不应一开始就交给另一个大模型评分。普通代码更便宜、快速、稳定,而且失败原因清晰。
当然,字符串匹配仍有局限:它不能自然处理"下午三点"和"15:00"的同义表达,也不能证明时间与标题的对应关系正确。后续还要升级为关系评测和语义评测。
九、今天踩到的工程坑:Python 版本也是契约
第一次创建虚拟环境时,系统默认 Python 是 3.9,SDK 导入阶段因为新类型语法失败。改用 Python 3.12 后才正常运行。
更有意思的是,同一份未锁版本的依赖文件,在不同 Python 版本下解析到了不同的 SDK 版本。
这提醒我:
text
只写 pip install xxx 不是可复现环境
至少应该锁定:
- Python 版本;
- SDK 版本;
- 关键依赖版本;
- 模型与接口传输方式。
当前实验最终完成了 12 个本地测试,覆盖成功、空结果、遗漏工具、无返回、错误参数、答案遗漏和已知幻觉等情况。
十、接下来的学习规划
接下来我会沿着"从 Demo 到生产 Agent"的路线继续推进:
第一阶段:完善单 Agent 质量闭环
- 从关键词评测升级到事实关系评测;
- 测试工具超时、网络异常与重试;
- 添加结构化输出;
- 建立多样本黄金测试集;
- 统计任务成功率、延迟和 Token 成本。
第二阶段:安全与人工审批
- 给发送通知等有副作用的工具增加审批;
- 学习输入、输出和工具 Guardrail;
- 测试越权访问、Prompt 注入和敏感信息泄露;
- 实现中断与恢复。
第三阶段:多 Agent 与框架对比
- 学习 handoff 和专家 Agent;
- 用 OpenAI Agents SDK 重构现有日报流程;
- 与 LangGraph 对比状态、恢复、审批和可观测性;
- 输出一份有真实测试数据的架构选型报告。
第四阶段:企业级 Agent 工程
- 接入 MCP;
- 引入持久化会话与长期记忆;
- 集成 OpenTelemetry/OpenInference;
- Docker 化部署;
- 建立线上反馈与回归评测闭环。
写在最后
今天的学习让我从"Agent 能调用工具"推进到了:
text
能调用
→ 能观察
→ 能验证
→ 能评测
真正的 Agent 工程,不是让模型显得聪明,而是让每一步都有证据、错误能够暴露、质量可以持续改进。
如果你也在学习 Agent,欢迎在评论区告诉我:你现在最头疼的是工具调用、MCP、工作流、记忆,还是评测?
后续我会继续公开记录从 OpenAI Agents SDK 到 LangGraph 对照、人工审批和生产评测的完整实践。觉得这条路线有用,可以收藏或关注,下一篇我们继续解决"轨迹正确,但事实关系仍可能错"的问题。