本周工作概览

第三周的主题是让Agent从“能调用工具”升级到“会编排工具”。上周跑通的基本流程是一次顺序调用,这周我发现很多分析场景下工具调用不是线性的——有时需要先并行查多个指标再综合判断,有时需要根据查询结果决定下一步查什么,有时查到的信息互相矛盾需要Agent自己分辨。这些需求把我自己写的那个简单ReAct循环逼到了极限,于是这周花了大量时间重构编排层,同时完成了日志检索工具的接入和三个工具协同的端到端调试。

具体工作内容

Agent编排层的重构:从顺序执行到有向图

上周的简单循环存在一个致命问题:每次只能调用一个工具,等结果回来才能决定下一步,而且没有记忆能力。这意味着如果Agent想同时查CPU和内存,它必须先查CPU、等结果、再查内存、等结果、再综合分析,整个过程串行执行,耗时翻倍。周五晚上我决定重写编排层,借鉴LangGraph的思路实现一个基于状态机的图编排引擎。

重构后的核心数据结构是一个AgentState,里面包含messages(对话历史)、collected_data(工具调用的累计结果)、pending_tools(待执行的工具列表)、current_step(当前执行到哪个节点)。整个分析流程被拆成若干个节点:analyze_alert(理解告警)、parallel_query(并行查询指标和日志)、retrieve_knowledge(检索知识库)、synthesize(综合分析)、generate_response(生成建议)。每个节点可以有多个前驱和后继,Agent在图结构上执行。

并行查询这个节点我单独实现了一个ToolInvoker,接收多个工具调用请求后并发执行,使用asyncio.gather同时发起PromQL查询、日志检索和状态检查,等所有请求都返回后再汇总结果。测试下来三个查询原本串行需要6到8秒(网络延迟加查询耗时),并发后压缩到2.5秒左右,提升明显。

日志检索工具的接入

日志检索这一块,另一位同学负责ELK栈的搭建,我负责封装Agent可调用的工具接口。考虑到日志数据量大、直接全文检索不现实,我设计的工具要求Agent必须提供三个关键参数:时间范围(比如告警前后15分钟)、服务名或实例IP、关键词列表(可选)。工具内部构造Elasticsearch查询语句,使用bool query组合时间范围和服务名过滤,再对message字段做全文搜索,最后返回匹配条数最多的高频报错模式。

为了控制返回给Agent的信息量,日志检索结果不会返回原始日志全文,而是返回一个聚合汇总:每种错误模式出现的次数、第一条和最后一条的时间戳、以及一条代表性样例。例如“Connection refused错误出现47次,时间集中在14:23到14:28”。这样Agent既能了解异常概况,又不会被成千上万条日志淹没。

调试中发现一个有趣的问题:Elasticsearch的默认时间字段是@timestamp,但我们的测试服务写入的日志时间格式不一致,有的用time、有的用timestamp。花了一个下午统一了日志采集侧的字段映射,最终所有日志都用log_time字段存储,工具内部只认这一个字段。

多工具协同的真实场景调试

这周最重要的成果是用一个真实故障场景把三个工具串起来跑通。场景是“订单服务接口错误率突增”。告警信息进入Agent后,编排引擎执行的第一步是analyze_alert节点,提取出关键信息:服务名order-service、异常指标http_requests_error_rate、阈值5%、当前值23%、发生时间14:30。

接着进入parallel_query节点,并发调用了三个工具:PromQL查询rate(http_requests_total{service="order-service",status=~"5.."}[5m])获取过去30分钟的错误率变化曲线;日志检索工具查询order-service在14:15到14:45之间包含errorexception的日志;状态检查工具查询该服务的Pod重启次数和当前实例数。三个结果几乎同时返回。

从PromQL返回的数据中,Agent看到错误率在14:28之前还低于1%,14:28到14:32之间飙升到18%,之后又回落。日志检索返回的高频错误是connection pool timeoutfailed to connect to database,共出现120多次。状态检查显示Pod没有重启,实例数正常。这些信息汇总到synthesize节点。

Agent的综合分析逻辑是这样写的:错误率短暂飙升后回落不符合服务持续崩溃的特征;高频的数据库连接超时提示问题可能出在数据库连接池;Pod未重启说明服务本身没有OOM或被驱逐。Agent推断大概率是下游数据库连接池在14:28左右被耗尽,后续请求等待超时,几秒钟后部分连接释放导致错误率回落。这个分析和我在模拟环境中预先注入的故障(手动压测导致连接池打满)完全吻合。

工具调用失败的处理策略

这周还加了一个重要的容错机制。之前工具调用失败就直接报错退出,现在改成失败后Agent可以尝试降级方案。比如PromQL查询超时(超过10秒未返回),Agent可以选择用缓存的历史数据替代,或者直接进入知识库检索阶段依赖已有案例给出建议。具体实现是在ToolInvoker里增加了超时控制和异常捕获,将错误信息包装成特殊格式的结果返回,Agent看到这个结果后会被提示“此工具调用失败,可尝试其他路径”。

有一种失败场景我特别处理了:当Agent反复调用同一个工具且每次都失败时,编排引擎会强制终止循环并在最终输出中告知用户“工具层异常,请人工介入”。这是为了防止Agent陷入无限重试的死循环。

前端联调与思考过程可视化

这周和前端同学一起梳理了Agent思考过程的数据结构。我设计了一个trace对象,每个节点执行时生成一条记录,包含节点名称、开始时间、结束时间、调用的工具列表、每个工具的输入输出摘要、以及Agent在这个节点的内部状态变化。前端把这些信息渲染成一个时间线组件,用户可以展开每个节点查看详细内容。第一次看到自己的Agent从“收到告警”到“给出结论”的完整脑回路被可视化在屏幕上时,还是挺有成就感的。

本周遇到的问题与解决

最头疼的问题是并行查询时的数据竞争。多个工具同时往AgentStatecollected_data里写结果,Python的字典不是线程安全的,偶尔会出现某个工具的结果被覆盖。改用asyncio.Lock保护写操作后解决。另一个问题是图编排的循环检测,一开始没有实现,结果某个测试场景下Agent在两个节点之间跳来跳去形成死循环,跑了十几轮才被我发现。加了一个visited_nodes集合和最大步数限制(最多执行10个节点)后问题解决。

更多推荐