别再只做会聊天的 Agent:我用 1 天把工具调用做成了可验证、可评测的工程系统

很多 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

含义分别是:

  1. 模型请求调用工具;
  2. Python 工具执行并产生结果;
  3. 模型基于结果生成最终消息。

我没有直接打印整个 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 质量闭环

  1. 从关键词评测升级到事实关系评测;
  2. 测试工具超时、网络异常与重试;
  3. 添加结构化输出;
  4. 建立多样本黄金测试集;
  5. 统计任务成功率、延迟和 Token 成本。

第二阶段:安全与人工审批

  1. 给发送通知等有副作用的工具增加审批;
  2. 学习输入、输出和工具 Guardrail;
  3. 测试越权访问、Prompt 注入和敏感信息泄露;
  4. 实现中断与恢复。

第三阶段:多 Agent 与框架对比

  1. 学习 handoff 和专家 Agent;
  2. 用 OpenAI Agents SDK 重构现有日报流程;
  3. 与 LangGraph 对比状态、恢复、审批和可观测性;
  4. 输出一份有真实测试数据的架构选型报告。

第四阶段:企业级 Agent 工程

  1. 接入 MCP;
  2. 引入持久化会话与长期记忆;
  3. 集成 OpenTelemetry/OpenInference;
  4. Docker 化部署;
  5. 建立线上反馈与回归评测闭环。

写在最后

今天的学习让我从"Agent 能调用工具"推进到了:

text 复制代码
能调用
→ 能观察
→ 能验证
→ 能评测

真正的 Agent 工程,不是让模型显得聪明,而是让每一步都有证据、错误能够暴露、质量可以持续改进。

如果你也在学习 Agent,欢迎在评论区告诉我:你现在最头疼的是工具调用、MCP、工作流、记忆,还是评测?

后续我会继续公开记录从 OpenAI Agents SDK 到 LangGraph 对照、人工审批和生产评测的完整实践。觉得这条路线有用,可以收藏或关注,下一篇我们继续解决"轨迹正确,但事实关系仍可能错"的问题。

相关推荐
DFT计算杂谈1 小时前
Janus单层Cr2SSe中的应变可调多压电效应与谷电子学
人工智能·算法·机器学习
今天AI了吗1 小时前
从 LLM 到 Agent Skill:把 AI 底层概念串起来
数据库·人工智能·sql·深度学习·神经网络·算法·机器学习
DS随心转小程序1 小时前
巧用 AI 导出鸭攻克各类难题完善 ChatGPT 输出 word 文档转化工作
人工智能·chatgpt·aigc·word·豆包·deepseek·ai导出鸭
梦想的旅途21 小时前
企微 API 二次开发:结合 AI 打造考勤打卡与报表智能分析系统
人工智能·企业微信
Kari111 小时前
腾讯云 ADP 实施问题解析:回答异常时企业如何组织排查与支持协同?
人工智能
码云骑士1 小时前
102-向量数据增删改查-增量更新-过期策略-向量漂移重索引
python
小唔w1 小时前
文件越攒越多?三步分类归档法 + 常用工具体验分享
人工智能
Molecular_Chat1 小时前
Biotin SNA 生物素偶联凝集素结合特异性、生物素标记效率定量表征研究
javascript·python
WILLF1 小时前
Python vs JavaScript 异常处理对比
前端·python