JOTO
Contact us
← AI 智库
AI 员工

第 18 篇 · 给 Agent 加上异常检测

2026 年 11 月 8 日

本文介绍如何为 AI Agent 构建运行时异常检测机制,涵盖异常定义、日志记录规范、七类检测规则(如连续工具失败、未知工具调用、上下文突增、接近轮次上限等),以及基于 Conversation Event 的状态化检查实现。强调异常检测需覆盖模型行为与工具执行双重维度,并保留完整上下文证据链。

上一篇我们给 Agent 加上了运行指标,可以看到最近的调用量、错误率和耗时。比如工具错误率升高了,我们就知道最近失败的调用变多了,接下来还得打开会话记录,查查哪些调用失败了、当时发生了什么。会话一多,靠我们逐条去查看就比较费时间。有些情况,比如同一个工具反复报错、上下文突然变大,可以先让程序检查出来,再把相关记录标出来,方便我们去查。这一篇我们就来给 Agent 加上这样的异常检测。

一、什么是异常,为什么要记录它

我们说的异常,包括哪些情况

我们先从平时开发的 ERP、CRM、订单等系统说起。比如我们写了一个提交订单的接口,用户填好信息,点击提交,程序校验完数据,把订单存进数据库,再返回提交成功。如果这时候数据库连接断了,订单没有存进去,程序就会报错。

这类异常我们比较熟悉。数据库连接失败、读取文件失败、调用外部接口超时,平时写代码时都会遇到。我们一般会用 try/except 或 try/catch 捕获异常,再决定是重试、结束这次操作,还是把错误继续交给上层处理。

不过,我们这一篇说的异常,范围还要更大一些。比如订单提交平时一秒就完成,这次却等了一分钟,程序虽然没有报错,但是系统还是给出异常警告,我们也得查查为什么这么慢。再比如接口返回成功了,订单金额却算错了,这类问题也需要异常排查。

只要实际运行情况和我们预期的不一样,就需要留意。 至于是不是异常,还得看具体业务。用户下单时库存不足,我们提示他无法购买,这是正常处理,库存明明足够,却因为数据库故障下不了单,就需要排查。查询一张订单和生成一份年度报表,我们能接受的等待时间也不一样。

以前我们是怎么记录异常的

还是提交订单这个例子。保存失败以后,我们可以在页面上告诉用户“提交失败,请稍后重试”。但后台如果也只记录了这一句话,等我们查问题时,还是不知道到底哪里出了错。

所以我们通常会把错误原因和异常堆栈一起写进日志。堆栈里有函数名、文件名和代码行号,我们可以根据这些信息找到报错的位置。除此之外,还要知道错误发生在什么时候,是哪一次请求、处理哪张订单时发生的。

比如我们从一条错误日志里,至少应该能看到这些信息:

时间:2026-09-10 10:23:15
级别:ERROR
请求编号:req-123
操作:保存订单
订单编号:order-456
错误:数据库连接超时

有了请求编号,我们就能继续找这次请求前后的日志。如果一次操作经过了几个服务,也可以用统一的追踪编号把它们关联起来。否则订单服务里一条错误、库存服务里一条错误,我们很难知道它们是不是同一次操作引起的。

系统简单的时候,这些日志可以先记录到文件里,系统服务多了,变复杂以后,我们也可以把它们收集到日志平台,方便统一查询。系统错误突然增多时,再通过监控告警通知我们,不用等用户反馈以后才知道系统出了问题。

前面我们说的慢请求、金额算错这类问题,就不能只靠捕获异常了。我们得另外记录接口耗时,对照业务规则检查金额,发现不符合要求时再留下记录。

为什么要记录异常

假设用户告诉我们“刚才订单提交失败了”,我们拿同样的数据再试一遍,却成功了。数据库可能只是短暂连接不稳定,外部接口也可能偶尔超时,等我们去查的时候已经恢复。如果当时没留下日志,很多问题就只能靠猜。

有记录就不一样了,我们先找到用户那次请求,看看错误发生在哪一步。如果是数据库连接超时,再查同一时间其他请求有没有报同样的错,就能知道是偶发问题,还是那段时间大家都下不了单。

把这些记录积累下来,我们还能发现一些平时不容易注意的问题。比如某个接口总在高峰期超时,或者更新了一版代码以后,同一种错误开始频繁出现

换成 Agent,我们还要记录什么

我们开发 Agent 时,前面这些错误一样会遇到。模型接口可能超时,工具可能没有权限读取文件,外部服务也可能暂时不可用。这些都要把错误原因记下来,并且能找到对应的会话和调用记录。

但 Agent 还有一些不同。以前我们写业务代码,调用哪个接口、参数从哪里来、失败后怎么处理,大多是提前写好的。Agent的很多步骤都是交给模型来决定,排查时我们就得多看一看,是工具本身执行失败了,还是模型调用的方式有问题?

比如模型调用了一个没有注册的工具,我们需要知道它调用的名字是什么、当时有哪些工具可用。如果工具提示参数错误,我们还得看模型当时传了什么参数。如果只保留一句“工具调用失败”,我们就不知道该改工具代码,还是该调整工具说明,让模型知道怎么用。

工具报错以后,Agent 接下来做了什么,也需要查看。读取文件时提示路径不存在,它找到正确路径再读,任务就能继续。如果一直用原路径读取,同样的错误出现了好几次,我们就得查它为什么没有调整做法。只记录最后一次失败,就看不出前面已经重复了多少次。 所以我们还要把相关调用连起来,记录这样的失败出现了几次。

有时候Agent也不报错,一直在运行,就是迟迟做不完。比如模型连续几次既没返回正文,也没有调用工具,或者一个任务跑了很多轮,马上就要达到我们设置的轮次上限。这些情况也要留下记录,方便我们排查它停在了哪一步、前面的轮次都做了什么。否则我们只知道一直没有结果,我们也不知道时间花在了哪里。

除了查看任务是否成功做完,我们还要看它花了多少时间、用了多少 Token。第 15 篇的旅行计划里,路线工具返回了大量内容,上下文从 2,208 Token 增长到了 26,200。那次工具调用是成功的,但后面模型每次请求都可能带着这些内容,输入一下子就多了很多。

如果我们只看成功或失败,这个中间的变化就被我们漏掉了。把增长前后的上下文长度记下来,再找到中间那次工具返回,我们才能看出是不是返回了太多不需要的内容,工具返回的内容是不是完全需要,是否可以压缩后在提供给模型。

这一篇我们先检查运行记录。当前代码实现有重复工具失败、未知工具、接近轮次上限、上下文突增、多次压缩、连续空响应和模型响应过慢七条规则。满足条件就保存一条 anomaly 事件,标出相关记录,方便我们回到会话里排查。规则命中只能说明需要留意,不能直接据此认定任务失败,也不代表已经找到了原因。

这些条件目前都是人工设定的,比如相同工具错误连续出现两次、模型响应超过 60 秒。它们是本项目的初始设置,还需要结合实际任务调整。记录里的 high、medium、low 用来区分关注优先级,不表示程序已经自动算出了故障影响。

下面我们来看看程序怎样保存这些信息,再用它们检查新记录。

二、从运行事件中检查异常

我们来看看当前系统是如何实现异常记录

异常检测仍然使用前面保存的 Conversation Event。主 Agent 的模型请求里有轮次、历史 Token 估算值和压缩信息,模型响应里有正文、工具调用和耗时,工具结果里有工具名称、返回内容和错误标记。

先确定检查哪些事件

这里先区分三个范围。一条会话由 session_id 标识,可以包含多次用户问答。用户每发来一次请求,stream_chat() 就开始一次执行。一次执行可能多次调用模型,iteration 从 1 开始记录这些模型迭代,一轮模型响应又可能包含多个工具调用。子 Agent 还有自己的执行循环和轮次。

当前检测器有两种入口,它们的状态范围不同。

入口状态从哪里开始哪些事件参与累计
运行时检查 detect_in_stream()每次 stream_chat() 新建状态本次用户请求中送入检测器的主 Agent 模型请求、模型响应和工具结果
历史扫描 detect_in_session()每次扫描新建状态,从会话文件开头重算整个会话中的相关事件,包括多次问答和子 Agent 记录,遇到用户消息不会重置状态

实时检查从本次用户请求开始计数。用户再发来一条消息,计数就重新开始。历史扫描则从会话文件开头往下读,即使读到下一条用户消息,也会接着前面的计数继续检查。

第 18 篇 · 给 Agent 加上异常检测

下面用同一次主 Agent 执行中的一组简化事件看一下这个过程,只列出本例关注的请求和工具结果,编号也是为了方便说明。

事件 m1  model_request  history_token_count=1000
事件 t1  tool_result    function_name=read_file  is_error=true  output="文件不存在"
事件 m2  model_request  history_token_count=1800
事件 t2  tool_result    function_name=read_file  is_error=true  output="文件不存在"

读到 m1 时,检测器记住上下文长度是 1000。读到 t1 时,记住 read_file 失败了一次,错误内容是“文件不存在”。

继续读到 m2,上下文从 1000 增长到 1800,增长了 80%,超过当前设置的 50% 阈值,于是产生一条 token_spike。读到 t2 时,相同工具又返回相同错误,连续次数达到 2,再产生一条 repeated_failure。

m1 → 保存上下文长度 1000
 t1 → 保存失败签名,连续失败次数为 1
m2 → 比较前后长度,产生 token_spike,保存新长度 1800
 t2 → 相同失败再次出现,产生 repeated_failure

这里的“连续相同失败”指相邻的工具结果都失败,而且工具名和错误摘要相同。中间出现模型请求不会清空计数;如果插入一次成功的工具结果,计数就会重置;如果换成另一种工具错误,则失败重新计数。

保存前面的状态

StreamDetectorState 里放的是计数、上一次的数值和相关事件编号。下面省略了部分成员。

class StreamDetectorState:
    def __init__(self) -> None:
        self.last_failed_signature = None
        self.failed_streak = 0
        self.failed_streak_event_ids = []

        self.empty_response_streak = 0
        self.compression_count = 0
        self.last_history_tokens = None
        self.last_model_request_event_id = None

        self.near_cap_fired = False
        self.compression_fired = False

事件编号用来保留依据。上下文增长需要前后两条模型请求,连续失败需要对应的工具结果。只有一个“发生异常”的提示,后面就很难找到当时的调用。

两个以 fired 结尾的字段表示是否已经提醒过。比如 Agent 到了第 40 轮时提醒一次接近上限,第 41 轮还满足条件,但不需要再写一条相同提醒。

这份状态属于内存对象,进程重启后不会保留。已经写入 JSONL 的异常记录仍然存在,历史扫描时可以读取原始事件重新计算状态。

三、连续工具失败与未知工具

_check_event() 先根据事件的 type 决定检查哪些字段。遇到 tool_result 时,读取 data.is_error。只有这个值为true,才会进入工具失败的处理分支。

连续相同失败

一次工具失败可能有很多原因。我们先实现一个简单判断,同一个工具连续返回相同的错误摘要,达到两次就记一条高等级异常。

代码用下面两个值组成失败签名,也就是用来比较两次失败是否相同的一组数据。

signature = (function_name, output[:120])

第一个值是工具名称,第二个值是错误输出的前 120 个字符。当前签名和上一次相同,就增加连续次数,不同,就从这次失败重新计数。

if state.last_failed_signature == signature:
    state.failed_streak += 1
    state.failed_streak_event_ids.append(event_id)
else:
    state.last_failed_signature = signature
    state.failed_streak = 1
    state.failed_streak_event_ids = [event_id]
    state.repeated_failure_emitted_at_streak = 0

假设 read_file 先返回“文件不存在”,下一次返回“没有读取权限”,虽然两次都失败,但签名不同,第二次会重新从 1 开始。两个不同工具返回相同错误,也不会被算成同一组连续失败。

这个只是我们当前的实现,你也可以修改实现不同的处理规则,假如只要是相同的工具名称连续失败就记录。

达到阈值以后,代码生成 repeated_failure,严重度为 high。判断条件如下。

if (
    state.failed_streak >= REPEATED_FAILURE_THRESHOLD
    and state.failed_streak != state.repeated_failure_emitted_at_streak
):
    state.repeated_failure_emitted_at_streak = state.failed_streak
    # 生成异常候选记录

REPEATED_FAILURE_THRESHOLD 当前是 2。这里并不是一组连续失败只提醒一次。第二次失败时写一条,第三次连续失败时,次数从 2 变成 3,又会写一条。

相同失败第 1 次  不提醒
相同失败第 2 次  repeated_failure,证据为第 1、2 次结果
相同失败第 3 次  repeated_failure,证据为第 1、2、3 次结果

如果希望一组连续失败只产生一条提醒,可以改成首次达到阈值时触发。当前实现会随着连续次数增加继续留下记录,查看异常数量时需要考虑这一点。

工具执行成功后,代码清空失败签名、连续次数和证据列表。因此下面的序列不会触发连续失败。

read_file 失败 → 任意工具成功 → read_file 再次失败

当前异常说明写着“以相似参数失败”,但实际判断没有比较工具参数。文件里虽然有 _args_signature() 辅助函数,规则并没有调用它。两次读取不同路径,只要工具名和错误输出前 120 个字符相同,也会被归为相同失败。

此外,同一轮模型响应可以一次返回多个工具调用。如果其中两个调用连续返回相同错误,也会命中规则。

所以当前规则只能帮助我们找到重复出现的工具错误,具体实施的规则大家可以按照项目的实际情况处理。

调用了不存在的工具

同一个 tool_result 还会检查错误输出里有没有“未知函数”或“未知工具”。

if "未知函数" in output or "未知工具" in output:
    # 生成 unknown_tool,严重度为 high

例如模型返回了一个没有注册的函数名,工具分发代码找不到它,就可能产生这种错误。也可能是工具配置已经修改,但会话历史里仍然保留着旧工具名称。

这条规则每遇到一条匹配的错误结果都会触发。连续两次调用同一个未知工具,第二条结果可能同时产生 unknown_tool 和 repeated_failure,分别标记未知工具错误和重复错误。因此这里两条异常记录不一定代表两个独立故障,它们可能是同一个错误引起的。

当前判断依赖中文错误文本。如果工具分发器换了错误文案,规则可能漏检,其他错误文本只要包含这些词,也可能被误归类。例如分发器的“未知函数类型”也会匹配“未知函数”,但它表示的并不是函数名没有注册,排查时要看完整错误。后面可以让工具结果带上固定错误码,检测器直接检查错误码,就不用依赖字符串内容。

四、模型轮次、上下文增长与压缩

遇到 model_request 时,检测器会检查三件事情,模型迭代是否接近上限、历史 Token 估算值是否突然增长,以及压缩累计次数是否达到阈值。检测所需的数据来自这条请求事件。

接近循环上限

前面的 Agent 主循环设置了最大模型迭代次数,避免任务一直运行下去。这里的轮次按模型请求计数,一轮可以调用多个工具,因此不等于工具调用次数。异常服务里也保存了一份上限和提醒比例。

MAX_TOOL_ITERATIONS = 50
NEAR_CAP_RATIO = 0.8

当前阈值为 int(50 * 0.8),也就是 40。模型请求中的 iteration 字段用于判断是否达到阈值。

JOTO 企业落地观察

  • 对企业部署意味着:Agent 异常检测不能仅复用传统服务的错误日志模式,必须将模型决策链(如工具调用序列、上下文膨胀、空响应轮次)纳入可观测性体系。否则运维团队面对‘任务卡住’只能回溯原始会话,无法快速定位是模型幻觉、工具配置漂移还是上下文失控。
  • 对智能体工程而言,异常检测模块的设计直接影响调试效率。文中强调保留事件ID链、失败签名及原始上下文快照,这要求企业构建Agent时必须将可观测性作为核心架构约束,而非事后补丁;否则当异常涉及多跳子Agent协作时,缺乏跨服务追踪能力将导致根因分析成本指数级上升。
  • 对RAG知识工程提出新要求:上下文突增(如从2k到26k Token)被列为关键异常,说明检索结果质量直接影响Agent稳定性。企业需在RAG pipeline中嵌入内容密度评估与冗余过滤环节,并将‘检索结果体积’作为可监控指标,避免知识召回成为隐性性能瓶颈。

立即咨询 JOTO

JOTO 提供覆盖企业智能体规划与搭建、AI 平台私有化部署、RAG 知识工程、AI 安全治理、FDE 驻场共创及持续运营优化的全周期 AI 落地服务,帮助企业把验证中的 AI 能力转化为安全、可控、可持续迭代的生产力。 联系 JOTO 获取 AI 落地咨询

想把这些做法用到你的业务里?

留下你的场景和痛点,我们帮你判断从哪一步开始。

联系我们
Contact Us

Start your enterprise AI rollout

Tell us your industry, team, and current pain points. We'll get back to you within one business day to help you decide what to tackle first, what data to prepare, and which platform fits.

WeChat
Scan to add us for a 1:1 chat
JOTO WeChat consultation QR code

Tell us what you need

Once we receive your details, we'll be in touch within one business day.