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 | 定位之后直接读取连续日志 |

浙公网安备 33010602011771号