Agent工具改进记录

背景

Doctor4j是一个用 Java 写的性能诊断 Agent。主循环已经搭起来了,模型可以完成调用工具->拿到结果->再决定下一步的闭合loop。

我给他的任务是:查请求耗时,给出日志行号,再推断一下根因。

结果跑了几次之后我发现,虽然能给出最终答案,但中间有些操作很绕,有些结论也需要回头检查。

我一边在 IDEA 里调试,一边改工具,优化调用过程。

项目:

  • Doctor4j:诊断 Agent。
  • target_service:被诊断的服务,里面有慢接口、N+1、锁等待、缓存等模拟场景。

参考资料:Anthropic 的 Writing effective tools for AI agents—using AI agents。之前看这篇文章记下了五条设计tool的原则:合适的工具、命名空间、有意义的返回信息、token 效率、打磨 description。今天正好试一试。

构建 context → 调模型 → 执行 tool → 回传结果 → 下一轮

只有一个search

第一版只有 logs_search(keyword, limit)。

实现很简单:逐行读日志,做普通字符串匹配,返回最近的几条结果。返回时带文件位置、行号和匹配总数,limit 限制数量。

任务大致是:使用日志工具查询请求的耗时,给出日志行号,推断根因。

第一轮它开始尝试“耗时”、“took”、“ms”等关键词,连续调用多次tool。前几个没找到,最后 ms 是靠运气命中了。拿到结果之后,它才知道这里的耗时字段是 cost=,接着去查订单相关的日志。

最终得到结果用了 3 轮对话,能看出锁等待的问题,也列出了最慢的请求:

767: request=GET /api/demo/orders/detail cost=294828ms status=200

不过,对这个接近 295 秒的请求,它当时还只是说可能是慢查询、N+1 或大结果集,需要继续查。

这也让我发现了第一个tool的设计问题,模型现在是一个个猜log的关键词。我根本没告诉他log的结构。所以下一步就是让模型自己去看log的结构

如果是人排查,通常会先打开日志看几眼。所以我觉得应该给它一个能直接看样本的工具。这里就是“合适的工具”——原来的 search 能定位内容,缺了一步了解格式的操作。

search+tail

接下来,我加了 logs_tail(limit)。

直接返回文件末尾几行。

lines 保留实际行号。truncated=true 表示文件前面还有内容没返回。数量还是限制在 1~100 行,看样本可以取 20 行。

description 也写了:不知道格式或关键词时,可以先 tail,再根据看到的内容搜索。

写desc的时候要把LLM当作一个初学者,要写的简单明了

这次模型开始看样本。第一轮还有一点“顺手搜几个常见词”的行为,但第二轮已经明确说:

日志格式已明确:request=... cost=...ms status=...。关键词应为 cost=。

符合预期。确实可以从实际日志中选择关键词了。

最终得到结果用了 6 轮,答案比之前详细,开始查最慢请求前面的 SQL,也能找到锁等待的异常。

但 tail 也有局限。这份日志最后 20 行大部分是异常堆栈,只有最后一行是请求结束记录。看到了样本,不等于看到了整个文件,更不等于根因已经确定。它还得继续找其他位置的证据。

我觉得这一步同时碰到了几条原则:补齐缺少的工具,返回来源和行号,只给少量样本,并且在 description 里说明什么时候用、结果代表什么。

为tool改名字

在进一步改进之前,我先改了一下tool的名字:

logs_tail   → tail_logs
logs_search → search_logs

只是试试把动作放前面,tail、search、read,一眼能看到做什么;后面的 logs 说明操作的是日志。这也对应命名空间和功能区分。

再跑一次,第一轮只调用了 tail_logs(20)。第二轮它说日志尾部有一个 cost=70019ms 的慢请求,上面还有异常堆栈,接着开始搜索请求耗时。

起步没问题。但后面它又一直匹配 GC、connection 等关键词,最后用了 8 轮。

这一次结果并不能说明什么,模型每次选择的路线可能不一样,现在还没做多次对照。

search、tail、read

加了read这个tool,接着看模型的调用

read_logs(startLine, limit)

如果想看第 767 行附近,可以:

{"startLine": 740, "limit": 30}

这样就把第 740~769 行连续读出来,空行、SQL、参数、结果都保留。到底从哪里开始、读多少,由模型根据已找到的线索决定。

返回里除了来源和行号,还加了 hasMore 和 nextLine。后面还有内容时,可以接着读下一段。

tool的描述中也有:起始行从 1 开始,包含该行;最多 100 行;到文件末尾就返回剩下的内容;超过末尾返回空列表;文件不存在则报错。实现会多读一行判断后面还有没有内容,这一行不放进当前结果。

search 的 description 加了一句:定位到行号以后,可以用 read 看附近上下文。还有 totalMatches 是匹配行数,不能当成 SQL 次数。

Anthropic 的文章还举过搜索时直接返回附近上下文的例子。我目前选择 search 定位、read 读取,后期可以比较一下两种方式。

这次能看到它先 tail,然后 search,定位几次关键词后开始 read。

符合我想要的效果。虽然也用了 6 轮。但是结论非常完整,还拿请求结束时间减去 294828ms,倒推出开始时间大约是 13:36:58.380,正好接近第 164 行订单查询的 13:36:58.381。第 166 行返回 200 个订单,后面逐个查明细。

锁的部分也把两次实验分开了:

  • 第 1407 行持锁 12 秒,第 1412 行更新等待约 11 秒后成功。
  • 第 1416 行持锁 70 秒,第 1419 行更新等待约 50 秒后超时。

之前那版把这两段混到一起了:14:17:26 已经结束的更新,不可能是在等 14:17:51 才开始的锁。这次没有再犯这个错误。

不过,最终答案仍有问题:

  • 它说 999801 在第 369 行,实际是第 765 行。第 369 行是 999933。
  • 说缺索引、全表扫描,目前还是猜测,得看表结构和执行计划。
  • 说改完能从 295 秒降到几十毫秒,这个没测过,不能直接保证。
  • 只看 /cpu 这个名字和耗时,也不能就认定一定是 CPU 问题,或者说它“非故障”。

所以工具能帮助它拿到更完整的信息,但引用和推断还需要检查。

总结

改动 模型请求轮数 变化
search 3 先猜关键词
search+logs_tail 6 能从样本发现 cost=,继续查 SQL 和异常
tail_logs、search_logs 8 第一轮只 tail,但后面还在反复搜索
tail_logs+search_logs+read_logs 6 定位之后直接读取连续日志
posted @ 2026-10-07 23:04  布鲁si  阅读(4)  评论(0)    收藏  举报