本文来自微信公众号: 叶小钗 ,作者:叶小钗,原文标题:《Agent Harness 可观测性:生产级 AI 项目必须补上的一课》
今年开始,Agent的基础执行环境从能力上已经OK,所以逐渐开始有很多产品真正的走向生产,这个时候如何让Agent长期稳定的运行,如何正确的执行长链路复杂任务就变得很重要了;
现阶段所有围绕Agent工程架构的技术被称为Harness,关于什么是Harness我们之前已经做过概括介绍,而今天我们将话题缩小,重点关注课题:Agent的执行可观测性。

该课题产生的原因来源于某产品负责人学员的感叹:
我终于知道,为什么搞不懂公司那批程序员在做什么了,他们在做技术架构的时候采用的是AI Max思路:
一个开源技术不行就换一个,单智能体不行就换多智能体,全部试过以后就说AI的上限就是这样,没有优化空间了,等新的技术开源了就再来一遍。
我有时候确实好奇,忍不住要问一他们怎么量化上限、有没有过程方法论?这批程序员就说量化不了、沉淀不了,都是别人的东西跑一下就好了。
我总觉得哪里不对,但因为不懂也说不出个所以然,只能听之任之,现在好了,确实不行,老子来给他们设计技术路径!
Agent的执行日志
最近我一直在开发Mini-Openclaw这个Agent项目,
目前基本的骨架已经搭建起来了,模型,工具,技能,记忆管理,会话压缩,多Agent协作等。
接下来准备做一些工具和skills来跑一跑案例,看看的Agent到底怎么样,能不能正常完成任务。
我先选择了一个简单的任务
让Agent帮我写一篇关于AI手机的文章,非常简单就是调用一个write_file的工具,把内容写到指定文件里。
然后Agent一直告诉我路径参数错误
Error:缺少'path'参数
文件一直没有写成功。
看到这个提示,感觉是路径问题,是不是目录不存在还是我的工具参数描述定义不清楚,
我仔细检查了一下这些代码,没有发现明显的问题。
按照我之前软件开发的经验,我需要去打印日志输出,看看模型返回了什么,工具为什么会出错。于是我接着去开发了,Agent执行日志和模型日志这两个模块的记录和显示,方便查看模型输入和输出,工具的执行结果等。
功能开发完成后,再执行一次我们写文章的任务,一下就看到了真正的原因:模型连续几次把工具参数包成了错误的_raw结构。

工具期待的是结构化参数,比如path和content。模型却把它们塞进了_raw字符串里。工具拿不到字段,自然失败。
这个问题挺简单的,模型返回的参数格式不对,程序需要做一下兼容就好了。
到这里,大家可能会认为以后遇到问题是不是就看刚刚开发的2个日志面板就可以了。
真实情况是还远远不够,Agent有没有按照我们的想法去完成任务,它的耗时,成本这些是怎么样的,所以给我们Agent做一个可观察的面板就非常有必要了:

什么是可观测性
可观察性这个说起来挺简单的,就是系统在运行的时候,我们可不可以在外面看出它里里面发生了什么。
我们开发其他软件的时候,常见三件套是指标、日志、链路追踪:
指标告诉你服务有多快,花费了多少钱,错误率多少等等
日志记录具体发生了什么事情
链路追踪记录调用关系,谁调用了谁,可以看出瓶颈在哪里
这三者结合起来基本就覆盖了大部分后端系统的故障排查需求:

非Agent程序有一个特性,就是它们的执行路径是固定,出错了我们还可以重跑复现问题,排查问题使用我们上面的3件套就可以了。
但是Agent不一样,同样的问题,每一次的执行路径都可能不一样,每一次输出的内容都不一样,甚至工具选择都不一样。
所以到底怎么来设计Agent的可观察性呢?在开始这个话题前,我们先探讨下模型的能力边界问题。
模型的能力边界
首先,大家要注意的是,可观测性并不是Agent所特有的,只要是AI项目,就都会有可观测的难题,这里就包括最传统的知识库RAG项目,但无论那一套的底层逻辑都是前面说的三件套。
在给学员上课的时候,最常说的一句话是:做AI应用一定要了解模型边界!这里所谓模型边界涉及了AI应用的两个流派:
AI Max:能用AI就用AI;
AI Min:能不用AI就不用AI;
所谓的可观测性,只在能不用AI就不用AI的模式下可行,他的背后体现的是模型的边界认知:追求完美准确率不现实,关键是要知道错在哪、为什么错、怎么改!并且能证明技术框架是闭环可重复的!
这里给大家举个例子,之前AI课的时候学员过多,需要一个排班系统,大概的需求是:
学员在微信群打出自己每天的空余时间,AI会主动统计大家都有空的时间,如果满足条件就预约会议,学员在群里的聊天信息如下:
A:20.00-22.00有空
B:18-20点没空,其他都可以
C:二十点后可以;
D:下午4点前没空;
E:我随便了,都行;
非常简单的需求,但就是这么一个简单的系统就能聊清楚什么是模型边界。
能用AI就AI
全部用AI就很简单了,直接一股脑丢给模型加一句“请问今天我该安排什么时间上课”就行,比如:

在简单场景下,AI Max是最优解,包括很多智能体如Manus在简单任务里面的表现是非常不错的。
随后就是,能不用AI就不用AI:
最小化AI应用
所谓最小化AI应用,就是只在不得不使用AI的地方使用,比如这里不得不使用的地方就是提取关键词,也就是语义识别每个学员的空闲时间:
A:空闲时间段为20:00-22:00(即晚上8点到10点)。
B:18:00-20:00没空,其他时间空闲(即00:00-18:00和20:00-24:00)。
C:二十点后可以,即20:00-24:00空闲。
D:下午4点前没空,即16:00-24:00空闲(下午4点为16:00)。
E:所有时间都空闲(即00:00-24:00)。
拿到空闲时间后,再自己用算法去做实现,这里马上就涉及了另一个问题了:在最小化AI应用的场景里,什么时候需要用AI?
答案很简单,在充满泛化场景的时候需要,比如上面ABCDE的回答,你很难用正则的方法给他匹配出来,类似这种关键词(关键知识)的提取只能依靠AI;
类似的场景是,我要求学员的昵称必须是学号-昵称-城市的格式,但学员一定会做得五花八门,比如就有学号_昵称_城市、城市_学号_昵称、学号昵称@城市等等莫名其妙的排布方式。
这种在学员自己设置后,也只有AI能快速帮他们做更正。
所有类似这种泛化要求较高的往往都必须AI出场,并且AI在这个领域做得挺好的!
那么,在这个基础之下,就可以讨论可观测性了:
可观测性
现阶段的AI项目其实有个巨大不确定性因素,因为大模型本身就是个巨大黑盒(还是概率输出)。
还是以上面排班系统为例,可观测性的价值在于:如果出现了AI识别不了的情况,能很快识别并解决!
比如现在出现一个F,他给的答案比较另类:
戌亥之时,余有暇。
类似于这种回答,模型很可能识别不了,那么排班系统就会出问题,这个在能不用AI就不用AI的模式下就可以被识别并优化。
这里的可以被识别且优化就是我们所谓的模型能力可观测。

上述是比较泛的模型可观测性介绍,理解他们后,我们再回归主题,进入Agent可观测性的讨论:
Agent可观察性
我们参考了其他系统的可观察性加上Agent的特性,Agent的可观察性大概包含以下几个部分:
原始日志数据,记录会话历史,模型的输入和输出,记录思考过程,工具的输入和输出
指标聚合,设计对应的指标,回答服务有多快,花费了多少钱,错误率多少等等
Trace调用树,回答谁调谁,调用链是什么样的
决策归因,这一步为什么要这么做?考虑过别的吗
任务状态流转,目标拆成了什么?哪一步在哪个状态?计划改了几次?
异常检测,Agent走了哪条路径?有没有打转/反复重试/死循环?
评估,这次到底成功了吗?输出对不对?
回放与对比,改一行prompt,重跑这个case,看会不会更好?
接下来我们逐步展开讨论:
原始数据的记录
我们开发的Mini-OpenClaw是单机、自托管、文件存储的项目,每一个会话保存一份jsonl包含用户请求消息,模型请求和响应,工具输入和输出,异常,会话压缩,评估结果这些原始内容。
这些都是原始数据,没有经过任何加工,只是记录了Agent的执行过程发生的所有动作
以下几类事件优先保留下来
model_call和model_result,记录模型输入输出、token、耗时、模型名。
tool_call和tool_result,记录工具名、参数、结果、错误。
上下文和状态变化,比如压缩、任务状态迁移。
观测系统自己生成的事件,比如anomaly、evaluation。
这类基础数据我们分成了2个部分一个模型的输入和输出,方便查看提示词是否按照我们设计的格式提供给模型

还有另外一部分是用来记录Agent的执行日志,可以按照时间线查看Agent的执行流程,我们可以按照时间的先后顺序完整的查看Agent是怎么一步一步思考,调用工具来完成任务的

指标设计
有了原始数据,指标很容易就能计算出来,我们需要哪些指标呢,我们简单设计了下面这几个指标
工具错误率
模型调用耗时
token消耗
上下文压缩是不是过于频繁
成本评估
这些指标很有用,它们能告诉哪里可能不正常,比如工具错误率很高,我们就需要去排查工具错误的原因,上下文压缩过于频繁,我们就要考虑上下文窗口设置是否合理,或者压缩算法有问题。

指标能提示我们哪里出问题了,但是不能告诉到底是什么错误
工具错误率30%,只能说明工具调用经常失败,它不能解释失败是工具实现坏了,还是模型传错参数,还是工具描述让模型误解了schema。
平均迭代次数变高,也不能直接说明Agent为什么打转。可能是任务本来变难了,也可能是它一直在重复同一个错误。
Trace调用树结构
到这里我们就不再满足于一个简单的事件列表日志,我们需要看到一次Agent的执行的调用树
Agent的执行不是一条线。它更像一棵树,一次模型调用产生一个或多个工具调用,工具结果进入下一轮模型调用,某个工具如果触发委派,下面还会展开一个子Agent的完整执行过程。
我们在设计原始日志保存的时候记录几个关键的字段
model_call_id
tool_call_id
delegation_id
model_call_id用来把一次模型请求、响应、决策记录和后续动作关联起来。tool_call_id用来把工具调用和工具结果配对。delegation_id用来把子Agent的事件挂回父Agent的委派节点。
有了这些字段,Trace调用树就不需要靠时间顺序来猜。
比如我们上次写文件失败的问题,如果放在Trace里看,会清楚很多。你可以展开某一次model call,看到它选择了write_file。再展开tool call,看到参数里出现_raw。再看tool result,发现工具返回字段缺失或参数解析失败。接着看下一轮model call,检查它有没有把上一轮失败纳入上下文。然后你会发现,它又生成了一次几乎同样的_raw。
这个时候,我们几可以很清楚的看到模型在重复错误参数结构。

如果没有Trace,我们还需要在众多的日志记录里面去翻看,查找对应的数据。有了Trace我们可以很清楚看到调用链路是怎么样的,排查问题的效率会有很大的提升。
子Agent也是同理。没有delegation_id,子Agent的事件会像一段插进来的噪声,而且这个是并行执行,只看时间线就会很乱了。你知道它做了什么事情,但很难知道它属于父任务的哪一次委派。挂成树以后,父子关系才稳定。
决策归因,为什么要这么做
通过Trace我们可以看到Agent的执行树,可以看到它不停的选择工具执行,这个时候我们自然就想知道模型选择这个工具的原因,它下次执行还会不会选择这个工具,还有没有其他的选择呢,特别是Agent如果把任务跑偏了要如何排查它在哪一步跑偏,它为什么会这样选择。
我们知道Agent做了什么,但不知道它为什么这么做。
对传统程序来说,行为来自代码。对Agent来说,很多行为来自模型当时的判断。
为了把为什么做记录下来,我们的做法是在system prompt里加入一个决策记录规范,让模型在需要选择动作时,在reasoning中输出一个固定格式的决策块。里面包括当前目标、候选动作、最终选择、选择原因和预期结果。

后端拿到模型响应后,会解析这个决策块,生成decision事件,再挂到对应的模型调用节点上。
这样看Trace时,就不只是看到它调用了write_file,还可以看到它当时认为自己的目标是什么,为什么选择写文件,而不是继续搜索或者询问用户。
我觉得这个设计是有价值的,它可以显示的告诉我们模型做出选择的原因。
我们把这个决策放到了reasoning里面。reasoning模型通常更容易输出这类结构,普通chat模型,系统只能退回启发式:从实际tool_call里拿到选择动作,再从reasoning或文本里截取一段理由。
当然我们不能完全相信这个决策,但对调试Agent来说,这已经比纯日志强很多。事实上在输出这个决策依据的时候,等同于大模型在自己反思,决策的正确率也会变高。

把任务状态显式化
对于简单一点的任务通过Trace面板就能看出问题所在,但是复杂任务光有Trace还不够。
比如我让Agent去搜索热点新闻,主Agent理解需求后,可能委派几个子Agent来完成任务,这时候用户真正关心的是任务状态:任务有没有完成?哪个卡住了?当前进行到了哪一步?
我们在系统中单独设计了任务状态。
每个会话开始时会创建一个root task。调用delegate_task时,会创建子task。任务有自己的状态机,从pending、planning、running,到waiting_child、succeeded、failed、cancelled。
PS delegate_task这个是我们Agent自带的一个工具,用来创建子Agent
这让Agent的过程从一串对话事件,变成了一个可以查看状态的任务系统。
同时项目里多了2个基础日志记录tasks.jsonl和tasks.json。前者记录任务变化历史信息,后者保存当前任务状态。
在主流程里,会话开始时创建root task,并在会话结束时把它转为succeeded。创建子Agent的时候同时创建子task,并根据子Agent的执行结果转为succeeded或failed。

异常检测
Agent一次工具调用失败很正常。工具schema没写清楚,模型第一次试错,或者环境里缺文件,都可能发生。真正危险的是它连续失败,不能收敛,还在不停的执行。
它可能同一个工具连续失败。也可能连续两轮模型都没有响应。也可能token突然暴涨。还有一种很常见的情况:它一直说自己在调整,但实际上只是换一种说法重复失败。
为了判断Agent有没有在正常执行任务,我们需要一个执行轨迹异常检测。
我们在项目里先实现了一组规则,比如重复失败、接近迭代上限、空响应循环、压缩频繁、未知工具。
这些规则会在执行过程中或者会话结束后触发。一旦命中,就写入anomaly事件。前端可以在可观察性页面展示,也可以在日志和Trace里定位。

评估
前面这些更多是在看过程如何。当我们看清楚过程后,还要回答一个更简单的问题:这次到底做对没有?
比如写文章这个任务,文件有没有写成功,内容是不是符合要求,中间有很多异常的话,最后结果还能不能接受。
所以我又做了评估体系,我设置3种评估方式
第一类是用户反馈。用户可以对模型的的回复点赞或者点踩
第二类是启发式评估。会话结束以后,系统会看有没有明显失败信号。比如没有最终回复,有高严重度异常,模型调用失败,迭代次数接近上限,工具错误率太高。
第三类是LLM-as-judge,让另一个模型来评估Agent输出
有了评估以后,优化才不只是凭感觉。
否则我改了prompt,只能说好像顺了一点,但不知道错误率有没有下降,打转有没有减少,最终成功率有没有提高。

回放和对比
真实调试Agent时,最常见的动作是:抓到一个失败case,改prompt,改工具描述,改Agent配置,然后再跑一次。
问题是,怎么判断改得有效?
严格复现基本不现实。模型有随机性,工具结果也可能不同,外部环境也在变化。
我们只能用同一个case,在相似条件下再跑一次。
具体做法很很简单:拿原来的user message,带上新的prompt、工具描述或Agent配置,创建一个新会话重新跑。跑完以后,对比两棵Trace调用树。
比如原来写文件失败了4次,新会话只失败1次,或者没有失败。
原来触发了高风险异常告警,新会话没有触发。
原来的评估是失败,新会话变成成功。
这样我们就能更清楚地判断,改prompt或工具描述到底有没有改善轨迹。
两个会话的迭代、模型调用、工具调用、委派节点会被结构化对齐。哪些节点相同,哪些新增,哪些消失,哪些状态或耗时发生变化,都可以展示出来。
这就让优化有了一个闭环:
从发现问题,定位轨迹,再修改配置,最后回放对比。
这里的回放更像是用同一个case做相似条件下的再次验证。它存在可控的偏差,不过已足够帮我们判断优化方向。

总结
这就是我在开发Mini-Openclaw这个Agent时候,对Agent可观察性的一些思考,目前开发的功能看起来都还比较粗糙,随着后续开发还会逐步的完善,目前实现的思路就是这些
日志记录
指标设计
链路追踪
异常告警
决策归因
任务状态
结果评估
回放对比
大家有什么好的建议可以评论区留言。