← 工具调用

工具调用日志要记什么?怎么避免日志拖垮业务?

中等 工具中心与动态注册 · 第 3 / 3 问 更新于 2026/09/29
工具调用可观测性Agent日志设计排障
本题落地项目AI Agentic RAG高级企业知识库平台

简化版

每次工具调用记一行:属于哪次运行(运行 ID、步骤 ID)、调了哪个工具(编码和名称的快照)、入参、返回、条数、成功还是失败、失败原因、耗时、时间。三条原则:成功失败都记,只记成功的话成功率永远是 100%,日志就没了排查价值;日志只是留档,不能改变业务结果:写日志失败不能让工具调用失败,记完失败日志也要把原来的异常继续抛出去,不能吞掉;控制体积:返回内容和错误信息按列宽截断,工具编码和名称在日志里存一份快照,工具被改名或删除后历史日志仍然能读。统计时注意口径:从没调用过的工具成功率应显示为空,而不是 100%。

详细版

tool_call_log
  run_id           属于哪次 Agent 运行(手动调试时为空)
  step_id          对应哪一个步骤
  tool_code        工具编码快照
  tool_name        工具名称快照
  request_params   模型或管理员传入的参数 JSON
  response_result  返回给模型的内容(截断)
  row_count        返回条数
  success          成功 / 失败
  error_message    失败原因(截断到列宽)
  cost_millis      耗时
  create_time      调用时间
原则做法不这样做的后果
成功失败都记两条路径都写日志成功率失真,失败无从排查
日志不改变业务结果写日志的异常自己吞掉;工具的异常照常上抛日志库抖一下,Agent 整轮失败;或者工具失败被悄悄吞掉
控制体积返回和错误按列宽截断超长写库报错,日志反而没了
快照日志里冗余编码和名称工具改名或删除后日志看不懂
统计口径零次调用成功率为空「没调过」和「全成功」混在一起

完整版教学

一、工具调用日志回答哪些问题

Agent 出问题时,排查几乎都从工具调用开始:

模型为什么查不到?        → 看它传了什么参数
模型为什么答错了?        → 看工具返回了什么,模型是不是读错了
这次运行为什么这么慢?    → 按耗时排序,看哪个工具慢
哪个工具最不靠谱?        → 按工具统计失败率
改了工具说明有没有效果?  → 对比改动前后的调用次数和参数错误率

这些问题决定了日志要记的字段:入参和返回用来看「模型看到了什么」,成败和原因用来统计,耗时用来找慢点,运行 ID 用来把一次执行的所有调用串起来。

二、成功失败都要记

一个常见的错误是只在成功路径写日志:

Object result = dispatch(toolCode, params);
logService.recordSuccess(...);     // 抛异常时走不到这里
return result;

工具失败时一行日志都没有,统计出来的成功率是 100%。正确的写法是成功和失败都进同一个写入口:

try {
    Object result = dispatch(toolCode, params);
    log.success(result, cost());
    return result;
} catch (Exception e) {
    log.failure(e.getMessage(), cost());
    throw e;                          // 记完继续抛,不吞异常
} finally {
    logService.record(log);           // 成功失败都只写一行
}

注意 throw e:日志只是留档,工具失败了,调用方(框架或 Agent 编排)仍然需要知道它失败了,才能决定是把错误交给模型、还是中断运行。

记忆钩子:日志要「全记」,但不能「改结果」。记完失败照样抛,写日志失败自己咽。

三、写日志失败不能拖垮业务

反过来,日志表本身也可能出问题:字段超长、库连接抖动。如果写日志的异常一路上抛,工具明明查到了数据,这次调用却失败了,Agent 可能因此整轮中断。所以写日志这一步要自己兜住:

public void record(ToolCallLog log) {
    try {
        log.setErrorMessage(StrUtil.maxLength(log.getErrorMessage(), 500));
        mapper.insert(log);
    } catch (Exception e) {
        logger.warn("工具调用日志写入失败", e);   // 只记警告,不影响工具返回
    }
}

两条规则并不矛盾:工具的异常要上抛(它是业务结果),日志的异常要吞掉(它不是业务结果)。

四、体积控制:截断要按列宽

一次知识库检索可能返回十几个片段,拼起来几十 KB;报错信息可能带着整段堆栈。估算一下:

一次检索返回 15 个片段 × 每段约 800 字 ≈ 12000 字
一次 Agent 运行调用 20 次检索 ≈ 24 万字,只是这一次运行的日志
错误信息列是 varchar(500),一段堆栈几千字,不截断直接写库报错

所以每个长字段都要按列宽截断:返回内容给一个上限(比如 6 万字),错误信息截到 500 字。截断的目的是让日志一定写得进去,完整内容如果真的需要,可以存对象存储再在日志里放引用。

五、快照:日志要能独立看懂

日志里只存工具 ID、展示时联表查名称,工具被改名或删除后,历史日志就变成一串看不懂的数字,甚至联表查不出来。所以日志里冗余存一份调用当时的工具编码和名称:

存法工具改名后工具删除后
只存 tool_id,展示时联表显示新名称,和当时不一致联不出来,显示空白
冗余存编码和名称快照显示调用当时的名称仍然可读

入参和返回同理:存原文,不要只存摘要,排查「模型为什么查不到」时第一个要看的就是它传了什么。

六、按运行串起来,按工具统计

日志表的两个主要查法决定了索引:

按运行查:这一次 Agent 运行调了哪些工具、顺序如何   → run_id 索引
按工具查:这个工具最近的成功率、平均耗时            → tool_code 索引

运行 ID 为空的记录来自管理端手动调试,和 Agent 自动调用区分开,统计时可以按需要排除。

七、统计口径:「没调过」不等于「全成功」

计算成功率时,分母为 0 的情况要单独处理:

工具调用次数成功次数错误口径正确口径
A100100100%100%
B00100%(或报错)空(显示为 -)
C403485%85%

B 从没被调用过,可能是说明写得不好、模型从来不选它,这恰恰是需要关注的信号;把它显示成 100% 就把问题藏起来了。

八、常见误区与追问

  • 误区:只记成功的调用就够了。 失败才是排查的重点,只记成功会让成功率永远是 100%。
  • 误区:记了失败日志,异常就可以吞掉。 日志只是留档,工具失败必须照常抛给调用方处理。
  • 误区:写日志失败也应该报错。 日志不是业务结果,写日志失败只记警告,不能让工具调用跟着失败。
  • 误区:日志字段存 ID,展示时联表就行。 工具改名或删除后历史日志会看不懂,要存编码和名称快照。
  • 误区:返回内容原样写库最完整。 超长内容会让写库直接失败,要按列宽截断。
  • 追问:从没调用过的工具成功率怎么显示? 显示为空,和「调用过且全部成功」区分开,没被调用本身就是需要关注的信号。
  • 追问:手动调试工具产生的日志怎么和 Agent 调用区分? 手动调用没有运行 ID,统计 Agent 行为时可以按运行 ID 是否为空区分。

九、加强记忆

工具调用日志每次调用记一行:运行 ID、步骤 ID、编码和名称快照、入参、返回、条数、成败、原因、耗时。成功失败都记,否则成功率永远 100%;记完失败照样抛,工具异常是业务结果;写日志失败自己吞,只记警告,不拖垮业务。长字段按列宽截断保证写得进去。按运行 ID 串一次执行,按工具编码做统计。零次调用的成功率显示为空,没被调用本身就是信号。

项目实战落地

项目里怎么做的

《AI Agentic RAG高级企业知识库平台》的工具统一走 ai/tools.py 里的 invoke 执行并记日志:

  • 成功失败都记:成功写入返回内容,失败写入失败原因;except 分支记完日志后 raise,异常继续传给调用方,日志不吞异常;
  • 截断:response_result 是 longtext,但一次检索返回十几个片段可能有几十 KB,写库前截到 60000 字;error_message 是 varchar(500),同样截断;
  • 中文可读:序列化时 ensure_ascii=False,日志页面上直接能读,不是一串 \uXXXX;
  • 关联运行:日志带运行 ID 和步骤 ID,关联到 Agentic RAG 执行链路的 agent_run、agent_step;日志页可以按工具、按调用结果、按运行 ID 筛选;
  • 成功率口径:按工具统计成功率时,从没调用过的工具返回空,页面显示为破折号。

《AI Agent 智慧医院智能导诊就诊系统》的 mcp_tool_call_log 冗余存了工具编码和名称,工具后来被改名或删除,历史日志仍然能看懂,查询也不用联工具注册表;手动执行时运行 ID 和步骤 ID 为空,一眼能分出哪些是调试、哪些是 Agent 自动调用。

为什么这样取舍

  • 失败也要记:只记成功的话成功率永远是 100%,日志就失去了排查价值。
  • 零次调用不显示 100%:「从没调过」和「调了 100 次全成功」是两件完全不同的事。

面试官还会追问

  • 调用日志表为什么建按运行 ID 和按工具编码两个索引?分别对应什么查法?
  • 知识库检索工具返回给模型时,为什么只保留片段 ID、文档 ID、分数和正文四个字段?

学完《AI Agentic RAG高级企业知识库平台》,上面这些追问你都会迎刃而解。

本题落地项目地狱锤炼AI Agentic RAG高级企业知识库平台基于企业知识库、PGVector 向量检索、Agentic RAG、FastAPI + LangGraph、Function Calling,实现向量 + BM25 混合检索与 RRF 融合、Rerank 重排、HyDE 与多查询改写、父子分块召回、LangGraph 状态图自适应检索、Agent 执行时间线和 LLM-as-judge 四指标评测,覆盖从基础 RAG 到高级 RAG 调优的完整闭环。FastAPILangChainLangGraphRAGPGVector源码+SQL喂饭学习教程配套面试文档环境安装文档项目运行文档 学习这个项目 也可以学AI Agent 智慧医院智能导诊就诊系统地狱锤炼 查看项目