文章

Agent技术内幕-一个运维排障Agent的设计与落地

Agent技术内幕-一个运维排障Agent的设计与落地

微服务架构下排查线上问题是一件很折磨人的事。一个用户反馈”订单支付失败”,你可能需要:

  1. 先查日志平台,定位到具体报错的服务和时间点
  2. 再查链路追踪,看这次请求经过了哪些服务、每个环节的耗时
  3. 翻 CMDB 找对应服务的实例信息和配置
  4. 登录 K8s 看 Pod 的运行状态和最近事件
  5. 结合监控指标判断是不是系统资源问题

每一步都要切不同的平台、记不同的查询语法、手动拼凑信息碎片。

排查过程本身并不需要多高深的技术判断,但【上下文切换成本】极高——等你把上述步骤走完,可能十几二十分钟就过去了。

我们要解决的正是这个问题:让 AI 替你完成这些机械的信息收集和初步分析工作,你只需要描述现象,剩下的事情交给系统去跑。

这篇文章会先用一个典型场景讲清楚这类系统怎么用,再深入剖析后端几个值得聊聊的设计决策。尤其是 ReAct 调度模型、事件驱动架构、工具错误恢复这几个点,对于想自己搭建类似 AI Agent 系统的同学也有参考价值。

为什么用 Go 而不是 Java

先聊一个选型话题。笔者的主力语言是 Java(母语),做中间件研发和微服务治理多年,照理说用 Java 写这个系统最顺手。

但最终慎重选择了用 Go 实现,核心原因有三个:

  1. Agent 的 IO 密集特征与协程天然匹配。 Agent 逻辑本质是重 IO——调 LLM、调工具都是等网络响应,CPU 多半闲着。goroutine + channel 几乎就是为这种场景设计的。Java 的 WebFlux 也在解决同类问题,但认知复杂度高太多,写起来很费劲。

  2. Go 生态有成熟的 AI 基础设施。 这个项目重度依赖 CloudWeGo Eino 框架来做 ReAct agent 编排,Eino 是开源社区维护的 Go 语言 AI 应用框架,提供了开箱即用的 ChatModel agent、工具注册、流式事件迭代器等能力。

  3. 个人语言偏好。 笔者对强类型语言有执念,职业生涯也基本只深耕两门——精通 Java、熟练掌握 Go。动态语言那套”跑起来再说”的体验,始终不太适应。所以即便抛开生态和并发模型的考量,选择范围其实也就锁定在 JVM 和 Go 之间了。

拿来主义——哪个生态的工具链最契合当前需求就用哪个,不因为自己熟悉 Java 就硬上。

宏观架构

系统的技术栈:

  • 后端:Go + Gin + GORM,AI 编排层基于 Eino 框架
  • 前端:React + TypeScript + Ant Design + Zustand
  • 部署:前端构建产物嵌入 Go 二进制,单文件部署

整体分层是经典的 Handler → Service → DAL 三层,但在 Service 层之上多了一个 ReAct 调度面,这是整个系统的核心:

flowchart TB
    subgraph 接入层
        UI[Web UI] ~~~ IM[飞书群]
    end
    subgraph ReAct层
        Runner[AgentRunner] ~~~ Bus[EventBus]
    end
    subgraph 支撑层
        MySQL[(MySQL)] ~~~ Tools[日志 · 链路 · CMDB · K8s]
    end

接入层 --> ReAct层
ReAct层 --> 支撑层

数据流一句话:用户发消息 → 落库 → 触发 ReAct 循环 → Agent 调工具收集信息 → 事件推前端实时展示。

用户视角:输入零门槛,手边有什么就发什么

很多所谓的”智能运维工具”有个通病——一边宣传流程自动化,另一边却要求你按特定格式输入。截图不行、URL 不认、必须精确写出服务全名和参数。用起来比不用还累。

我们的定位不一样:你平时怎么跟同事沟通,就怎么跟它沟通

  • 同事在群里贴了一个 APM 链路链接,你直接转发给机器人:”帮我看下这个 trace 有什么异常”
  • 告警系统推了一条 Pod 重启通知,把发布平台链接丢进去:”为什么这个 Pod 被 OOM 了”
  • 用户反馈了个报错截图,截图发过去,多模态模型直接理解图片内容然后排查
  • 什么都没,你就说一句”支付服务最近 30 分钟有没有异常”,它也能从 CMDB 查到应用名、拉日志、check 链路

输入形式无所谓——纯文本、链接、traceId、截图——系统会根据你给的东西自动判断该调用哪些工具、从哪开始查。这就是后面讲的 page_url 自动解析的应用层价值。

一个完整的排查例子

假设你在聊天群里收到一条告警:”支付服务超时率突增”。你直接把告警消息转发给机器人:

payment-service 超时率涨了,帮我看看最近 30 分钟什么情况

系统的工作流:

  1. 创建 Task,进入 ReAct 循环
  2. 第一轮推理:LLM 理解需求,决定先查日志平台,筛选 ERROR 级别
  3. 拿到日志结果,发现大量 Connection timeout,判断需要确认下游依赖状态
  4. 第二轮推理:调用 CMDB 查 payment-service 的下游依赖,发现依赖了 inventory-service
  5. 第三轮推理:调用链路追踪查 inventory-service,P99 延迟从 50ms 飙升到 3s
  6. 第四轮推理:调用 K8s 查 inventory-service 的 Pod 状态,发现最近有一次滚动更新,拉取 K8s 事件看到镜像拉取重试
  7. 输出结论:inventory-service 新版本部署异常,建议回滚

整个过程就一句话的事。每一步的推理结果和工具调用输出都会实时推送到前端,你可以随时看到 AI 的思考过程,随时打断、追问

这个系统不替你做决策

AI 的职责是收集信息、关联线索、给出推断。真正要执行的操作——回滚服务、重启 Pod、切流量——需要你来拍板。

这是运维场景的人机协同定位:AI 负责把”该看什么、看到了什么、说明了什么”讲清楚,决策权始终在人手里。

典型工具能力矩阵

核心链路覆盖如下工具能力:

领域能力
日志分析按应用名、traceId、接口、IP 等多维检索;支持直接粘贴日志平台分享链接
链路追踪链路概览(耗时/span数/错误数)、可视化调用树、单 Span 深度钻取(入参出参/异常堆栈)
CMDB应用模糊搜索、元信息查询、集群与环境列表
K8sPod 运行状态与镜像版本、Pod 事件时间线(崩溃/OOM/镜像拉取失败)、发布变更记录;支持粘贴发布平台链接
服务注册服务搜索、实例详情、IP 反查服务

开发者视角:几个值得聊聊的设计

下面深入到后端实现,挑几个花了心思的设计展开讲讲。

ReAct 调度模型:三层分工,Session 级互斥

ReAct(Reasoning + Acting)这个概念本身不新鲜,但把它落地成一个稳定、可控的生产级系统,需要处理不少边界情况。

调度层设计分三层:

第一层:Manager(门面)

对外只暴露一个方法:收到新消息后触发 OnNewInput(sessionID)。消息 Handler 把消息落库之后调一下就完事,完全不感知后面的执行细节。典型的外观模式。

第二层:ReactLoop(Session 级调度)

这里是核心。每个 Session 在进程内存中只允许一个 agent 实例在跑。实现方式是维护一个 runningSessions 集合,加锁检查——如果 session 已在运行,新消息直接跳过,不启动第二个 agent。

第三层:AgentRunner(Eino 执行)

基于 Eino adk 构建 ReAct graph,注入全部工具,最多 50 轮迭代。执行结果通过事件迭代器流式输出,每个 event 对应一轮推理或一次工具调用。

工具设计:有机整合胜过无脑堆砌

工具是 Agent 系统的”手”,工具设计的好坏直接决定了系统能走多远。这一节展开讲讲在工具设计上踩过的坑和沉淀下来的思路。

一个直觉是”工具越多,Agent 越强”。但实际上,简单粗暴地堆砌工具反而会起反作用:

  • LLM 选择困难。 18 个工具的函数描述加起来可能有几千字,每次推理都要在这么多候选中挑一个最合适的。候选越多,挑错概率越大。
  • 调用链不可控。 如果工具之间没有明确的先后约束,LLM 可能会走出你意想不到的路径——比如还没查 CMDB 就先去翻 K8s,查了一堆 Pod 发现连应用名都不确定。
  • 上下文膨胀。 每个工具的输出都会追加到对话历史里,没有节制的工具链会让上下文快速膨胀,后面的推理质量断崖式下降。

关键不在于工具的数量,而在于工具之间怎么衔接——能不能让 LLM 自然地沿着一条”信息密度递增”的路径走下去,每一步的输出都恰好是下一步的输入。

跨工具信息传递:让工具之间”认识”彼此

单个工具的分级设计只是基础,更关键的是不同工具之间能不能自然串联

工具之间的数据传递靠的是 LLM 的推理能力——但前提是工具的输入输出设计要支持这种串联

  • 日志工具的 ip 参数描述中写着”可从 K8s Pod 查询工具返回的 Pod 列表获取”——LLM 看到这句就知道先查 Pod 再过滤日志
  • 链路概览工具的 trace_id 参数描述中写着”可直接使用日志工具返回的 trace_id”——LLM 拿到日志后自然会提取 trace_id 调链路工具
  • CMDB 应用元信息工具的描述中写着”若应用名还不明确,先调用模糊搜索工具”——避免了 LLM 拿不精确的应用名直接查

工具之间的参数描述要互相引用。这就像给 LLM 画了一张”工具地图”——不是每个工具都孤立地写自己做什么,而是告诉 LLM 在什么情况下从哪个工具的哪个字段拿到数据喂给我。这种”工具间联动”的提示,比写再多的工具能力描述都管用。

工具协作:trace 三件套的渐进式查询

以链路追踪为例,设计了三个工具,不是平级的,而是有明确的先后关系:

第一步: trace_summary (链路概览)。只返回最轻量的摘要:总耗时、span 数、错误数、起始时间。这几个数字决定了后续策略——有报错就先看错误子树,没报错就看全量树做性能分析,span 数太大的先看骨架避免上下文爆炸。

第二步: trace_detail (调用树详情)。拿到摘要后,LLM 根据 error_countspan_count 智能决定 only_errordetail_level 参数。这里的关键是工具描述里已经写好了决策规则,LLM 不需要自己”悟”,照章办事就行。

第三步: trace_span_detail (单点详情)。从调用树中锁定异常 span 后,最后一步才钻取这一个 span 的详细信息——异常堆栈、标签、出入参、容器信息。diagnostic_level 同样支持 1/2/3 级渐进。

三个工具形成了一个先宏观 → 再定位 → 后钻取的信息漏斗。每一步都是必要的,上一步的输出直接决定下一步的参数,LLM 不需要在迷宫里乱撞。

分级输出:按需取舍,避免上下文爆炸

上面的 trace 工具都支持 detail_leveldiagnostic_level 参数。这不是拍脑袋加的,而是踩过坑之后的补救措施。

举个例子,trace_detail 返回 span 调用树,最严重的情况是一个 trace 有 6244 个 span——如果每个 span 都带完整字段,直接就能把 LLM 上下文打爆。所以引入了三级过滤:

level返回的 span 字段适用场景
1span_id、parent_id、服务名、耗时、状态码快速扫一眼调用结构,锁定异常服务
2额外增加:标签名、开始时间、Pod IP、集群名标准分析,兼顾调用关系和定位信息
3全部元数据(不含 children 子树)调试兜底,只有前两级不够时才升级

单点详情工具的 diagnostic_level 也是类似思路——level 1 只看异常日志和关键属性,level 2 加上出入参和进程信息,level 3 才拉全量。而且每级之间是手动升级的:LLM 要显式传更高的 level 才会返回更多数据,不会自动”帮”你升级导致上下文悄悄膨胀。

换句话说,分级输出的本质是把”要多少信息”的控制权交给 LLM,而不是工具一股脑全吐出来。

错误安全包装:工具挂了不影响主循环

工具调用失败是家常便饭。查日志 ES 超时、调 CMDB 拿不到数据,这些都很常见。

传统的做法是工具抛异常,agent 收到异常事件,循环中断。

我们的做法是:每个工具外面包一层错误处理装饰器:

1
2
3
工具执行返回 (result, error)
  ├─ 有 error → 不向上抛!转成 JSON: {"tool":"xxx","status":"error","error":"...","hint":"..."}
  └─ 正常 → 透传 result

返回的错误 JSON 长这样:

1
2
3
4
5
6
{
  "tool": "log_query",
  "status": "error",
  "error": "ES query timeout after 30s",
  "hint": "工具执行失败,请根据 error 字段判断原因:若为参数错误请调整参数后重试;若为外部系统错误可稍后重试或改用其他工具;若持续失败请向用户说明当前工具暂不可用。"
}

LLM 拿到这个结果后可以自行判断下一步——换个查询条件重试、跳过这个工具换另一个、或者如实告诉用户日志平台暂时不可用。整个 ReAct 循环不会因为一个工具挂了就全盘崩溃。

结果截断与启发式聚焦:不要让无效数据淹没 LLM

分级输出解决的是”主动控制返回量”的问题,但还有一个更棘手的场景:用户给的查询条件太宽了。比如日志查询没指定时间范围、没过滤 log level,工具查回来几万条日志,直接返回的话上下文立刻打爆。

很多系统的做法是”截断”——返回前 N 条,后面的默默丢弃。但这样做是假成功:LLM 只看到了截断后的片段,它不知道数据被截了,还以为这就是全部结果——推导出的结论可能完全是错的。

我们的做法是先执行,再判断。工具正常执行,拿到结果后统一过一层装饰器:计算返回结果的 token 和 byte 总量,如果超出阈值,工具不返回数据,而是返回一个结构化的”结果过大”错误。

提一下 token 怎么估算的:不用 LLM tokenizer,而是按字符类别粗略估算——ASCII 字符约 4 字符/token,CJK 字符约 10 字符对应 8 token,其他字符约 5 token/10 字符。工具有效负荷主要是 JSON,这种粗粒度估算够用且零依赖。

工具超限时返回的 JSON:

1
2
3
4
5
6
7
8
9
10
11
{
  "tool": "log_query",
  "status": "error",
  "error_type": "result_too_large",
  "error": "tool result exceeds max token limit",
  "actual_tokens": 18500,
  "max_tokens": 8000,
  "actual_bytes": 48000,
  "max_bytes": 32000,
  "hint": "工具返回结果过长,禁止截断返回。请调整参数后重试,例如:缩小时间窗口、增加过滤条件、优先查询摘要、降低 detail_level 或减少 page_size。"
}

也就是说,宁可 fail-fast 也不返回不完整数据。LLM 看到这个 JSON 后,可以从 hint 和实际数据量中自行判断怎么收窄——加 ERROR 级别过滤、缩小时间窗口到 5 分钟、先查摘要再决定方向。这比”偷偷截断然后 LLM 基于不完整信息做判断”要安全得多。

这跟 MySQL 先把结果集拉出来、发现太大后告诉客户端”你该加 LIMIT”是一个思路——先完成执行,再在工具层统一做结果校验。

URL 自动解析:降低工具使用门槛

Agent 系统的另一个常见痛点:用户发给你一个平台链接(比如 APM 链路详情页、日志平台搜索页的分享链接),你让 LLM 调用工具去查,LLM 需要手动拆解这个 URL 里的参数——哪个是 trace_id、哪个是环境编码、时间范围是多少。让 LLM 做 URL 解析,就像让一个刚学开车的人同时看地图和路况——不是不能做,但出错率高。

我们的做法是:工具直接接受 page_url 参数,解析工作下沉到工具层

trace_summary 为例,它同时接受 trace_idpage_url 两个参数,二选一即可。用户直接贴 APM 详情页的浏览器地址栏链接,工具内部 ParsePageURL 自动拆出 trace_id 和环境信息,不需要 LLM 手动做任何解析。

日志工具同理,支持两种调用模式:

  • 结构化字段模式:LLM 收集好环境编码、应用名、时间窗口、过滤条件后调用
  • page_url 模式:用户直接给日志平台分享链接,工具解析 URL 里的所有查询参数,LLM 零负担

这个设计带来的好处很直观:

  • LLM 不需要理解不同平台的 URL 格式——那个活丢给工具层做,工具层随时可以更新解析逻辑
  • 用户操作更自然——群聊里贴个链接 @ 机器人问你看到了什么异常,不需要手动描述查询参数
  • 工具返回时还会把解析后的 page_url 带回 output,方便 LLM 后续拼接给用户一个”点这里看详情”的可点击链接

这是一种把协议复杂度从 LLM 层下移到工具层的思路。LLM 擅长推理和决策,不擅长精确的字符串解析。URL 格式、时间格式、环境编码这些机械活,应该由工具代码来处理,LLM 只管”给什么参数能查到什么结果”这个层面的判断。

Shell 命令安全:两重防线挡住注入攻击

Agent 需要执行 Shell 命令来做网络探测——ping 个 IP、curl 检查健康检查端口、dig 个域名。

最直接的做法是让主 Agent 生成 bash 命令,但这里有个安全隐患:用户的对话内容可能被精心构造,绕过了 Agent 的安全指令。

比如用户在排查问题时”无意”提到”帮我看看 /etc/passwd 有没有异常用户”,如果主 Agent 把这个意图转成了命令去执行,后果很严重。

我们的做法是不让主 Agent 碰命令生成system_bash(subAgent) 工具内部嵌了一个便宜的 Flash 小模型,带着严格的安全 System Prompt:

  • 只接受自然语言的”探测意图”(比如”ping 10.0.0.1”、”查一下 example.com 的 DNS”)
  • 看不到主对话的任何上下文,不知道用户之前聊了什么——注入攻击根本传不到它这层
  • 只被允许生成只读探测命令(curl GET、ping、nslookup、date),输出限制为单条 bash 命令

Flash 模型生成的命令还要过第二层——黑名单正则检查。rm、kill、sudo、chmod、wget、curl -o 这些危险模式直接拒绝,不管 Flash 模型出于什么原因生成了它们。

sequenceDiagram
    participant M as Main Agent
    participant S as SubAgent
    participant B as Bash

    M->>S: 自然语言描述需求
    S->>S: 生成 bash 命令 + 黑名单检查
    alt 通过
        S->>B: 执行命令
        B-->>S: stdout + stderr + exit_code
    else 命中危险模式
        S->>S: 跳过 Bash,生成拒绝原因
    end
    S-->>M: 返回 JSON(执行结果或拒绝原因)

SubAgent 一定会返回结构化的 JSON 结果。正常执行时:

1
2
3
4
5
6
{
  "probe_intent": "查询当前服务器时间和时区",
  "command": "date '+%Y-%m-%d %H:%M:%S %Z'",
  "output": "2026-05-23 16:30:00 CST",
  "exit_code": 0
}

命令执行失败(exit_code 非零)也一样返回,不抛异常:

1
2
3
4
5
6
{
  "probe_intent": "检查服务端口 8080 是否正常监听",
  "command": "curl -s --max-time 10 http://10.0.0.1:8080/actuator/health | jq -r '.status'",
  "output": "curl: (7) Failed to connect to 10.0.0.1 port 8080 after 10003 ms: Connection refused",
  "exit_code": 7
}

四个字段各司其职——probe_intent 是传入的自然语言意图(原样返回),command 是 SubAgent 内部 LLM 生成的 bash 命令,output 是 stdout+stderr 合并输出,exit_code 0 表示成功、非零表示失败。

Main Agent 拿到这个 JSON 后自行判断下一步——exit_code 非零就走错误恢复流程,命中危险模式就看拒绝原因决定是否换个方式重试。

这是一种用 LLM 做输入净化器的思路。主 Agent 的上下文是”脏”的——用户消息、工具结果、多轮推理,你不知道哪个角落埋了注入点。与其在主 Agent 的 Prompt 里罗列一百条安全规则(总有办法绕过),不如把命令生成这个高风险操作完全隔离到一个干净的、受限的独立 LLM 里——它只管把一句意图翻译成一条命令,别的什么都不知道。

这个模式可以推广到任何”Agent 需要把自然语言转成可执行代码”的场景——SQL 生成、脚本执行、配置变更——用一个受限的小模型做翻译层,比在 Prompt 里跟主模型斗智斗勇要可靠得多。

协作式中断:温柔地打断 Agent

用户在一个正在执行的会话中发了新消息,怎么让当前 agent 停下来?

最粗暴的做法是直接 cancel 上下文,但这会导致当前工具调用的中间状态丢失。我们用的是协作式中断:

在 agent 的中间件里,每次 LLM 发起新一轮调用之前,检查一下数据库里有没有新的 pending 消息。如果有,返回一个特定的哨兵错误。

这个哨兵错误在事件处理循环中被专门捕获:

sequenceDiagram
    participant DB as 数据库
    participant R as ReAct Loop

    R->>DB: 每轮 LLM 调用前检查 pending 消息
    DB-->>R: 有 pending
    R->>DB: 旧 Task 和 Step → canceled
    R->>R: 回到消费循环,新消息启动新 Task

整个过程状态完整保留——旧 Task 被标记为 canceled 而不是直接删掉,用户回头看聊天记录时能看到完整的执行过程;新的 Task 基于新消息立即启动。对用户来说就是我发了新消息,旧的分析停了,新的开始了。

子 Agent 迭代守卫:给 Agent 一个体面的退场机会

源码搜索这类子 Agent 可能在一个大仓库里反复 grep/cat 停不下来。简单地给个超时强杀不是好方案——中间结果全丢了,还不如别跑。

我们的做法是给子 Agent 一个”体面的退场机会”。在 Agent 中间件里注入一段逻辑:当迭代计数接近上限时(还剩最后 1 轮),在 Agent 的消息列表里追加一条 System 消息:

这是你的最后一轮,禁止再调工具,基于已有检索结果给出最佳结论。

Agent 收到这条消息后不再调用工具,而是认真整理已经拿到的源码片段,给出当前能给出的最好答案。

中间结果没有被丢弃,Agent 也没有被粗暴打断。

sequenceDiagram
    participant A as 子Agent
    participant T as 工具

    loop 迭代中
        A->>T: 调用工具
        T-->>A: 返回结果
        A->>A: 检查迭代计数
    end
    A->>A: 接近上限,注入 System 消息<br/>"这是最后一轮,禁止再调工具"
    A->>A: 停止调用工具,整理已有检索结果
    A-->>A: 返回当前最佳结论

这个模式的妙处在于通过修改 Agent 的内部状态来控制行为,而不是从外部强行终止。比超时强杀更优雅,比调整 Prompt 更动态可控。

透明数据脱敏:非侵入式的安全层

Agent 调用工具拿到结果后,这些结果会追加到 LLM 的上下文中。问题是工具返回数据里可能含着手机号、身份证号、数据库连接串之类的敏感信息——日志里能搜到、CMDB 里能查到、链路追踪里能看到。如果不在进入 LLM 之前脱敏,这些数据就会永久留在对话上下文里。

sequenceDiagram
    participant T as 工具层
    participant P as 脱敏代理
    participant M as LLM 模型

    T->>P: Tool 消息结果(含敏感字段)
    P->>P: 深拷贝消息,正则脱敏
    P->>M: 脱敏后的 Tool 消息
    M-->>P: Assistant 消息(不处理)
    P-->>T: 原样透传

做法很直接:在模型调用层插入了一个代理层,所有发给 LLM 的消息——无论是用户输入还是工具返回结果——先过脱敏正则再传入模型。工具和模型本身完全不知道自己发送/接收的数据被处理过。

这里有两个细节值得一提:

  • 只脱敏 User 和 Tool 角色的消息。System 消息是系统注入的、Assistant 消息是 LLM 自己输出的,这两类不用处理。
  • 消息必须做深拷贝再修改——Agent 多轮循环中内部持有同一条消息的引用,原地修改会污染后续轮次的原始数据。

跟工具错误包装是同一种思路——用代理模式在关键路径上插入横切关注点,调用链路完全无感知。

Prompt 工程:像写代码一样写指令

系统 Prompt 不是一大坨文本,而是按数字前缀拆成了多个 .md 文件:

1
2
3
4
5
6
7
system/
├── 100-role.md          # 角色定义
├── 200-style.md         # 回复风格
├── 300-execution.md     # 执行规范
├── 350-tool-routing.md  # 工具路由策略
├── ...
└── 900-frameworks.md    # 框架/业务知识

启动时按数字顺序加载、用分隔符拼接。每个文件职责单一,修改角色定义不会误伤工具路由规则,新增指令也只需要加一个文件。这比维护一个动辄几百行的 Prompt 文件要友好得多。

900-frameworks.md 还会额外加载运行时目录下的动态内容(当前支持的环境列表、服务目录等),注入到 Prompt 末尾。

另外,如果会话来自飞书群,还会追加一段频道上下文(群名、群人数、是否工单群等),帮助 LLM 理解对话背景。

其他值得一提的小设计

  1. Streaming Step 懒创建。 推理 step 在收到第一段文本内容时才创建,不是一开始就建一个空的。每个 agent event 对应一个推理 step,event 结束后关闭——形成每轮推理一个 step 的自然粒度,前端展示的思考过程节奏感很好。

  2. 单文件部署。 前端 Vite 构建产物嵌入 Go 二进制,最终部署只需要一个可执行文件。没有 nginx 配置,没有 node 进程,一个 ./server 就跑起来了。

总结

这篇文章从用户和开发者两个视角拆解了这类 AI Agent 系统的设计思路。核心就三条线:

对用户来说,输入零门槛。 截图、URL、traceId、纯文本,手边有什么就发什么,不用学格式。URL 自动解析把协议复杂度下沉到工具层,LLM 只管推理,不管字符串拆解。

对工具设计来说,有机整合胜过堆砌。 工具之间靠参数描述互相引用画出”工具地图”,让 LLM 沿信息密度递增的路径自然走——先宏观再定位后钻取。分级输出和 fail-fast 模型防止上下文膨胀,错误安全包装保证单个工具挂了不影响主循环。System Bash 的双层安全机制用独立小模型做净化层,把注入风险和主对话完全隔离。

对系统安全与可控性来说,兜底机制决定下限。 透明数据脱敏代理在模型调用的关键路径上无感知地切断敏感信息泄露;子 Agent 迭代守卫用中间件注入的方式给 Agent 一个体面退场的机会——不是从外部强杀,而是让 Agent 自己意识到”该收尾了”。

说白了,Agent 系统不是模型越强越好,而是工程约束加上去之后还能稳定跑

本文由作者按照 CC BY 4.0 进行授权