Administrator
发布于 2025-12-14 / 7434 阅读
123

AI Agent 架构设计:从 ChatBot 到自主智能体

把工作流改成 Agent 三个月后,我们又改回去了一部分

去年我们的运维平台是一套硬编码的工作流:告警触发 → 按告警类型走固定的排查步骤 → 生成结论 → 通知。今年八月我们把它改造成了自主 Agent,让它自己决定调用什么工具、走几步。

跑了三个月,结论比较复杂:有些场景效果好得出乎意料,有些场景 Agent 明显不如工作流,最后我们把一部分场景改回去了。这篇讲这个决策过程,以及我对 Agent 架构边界的理解。

先说清楚两者的本质区别

很多人把「用 LLM 的系统」都叫 Agent,这个混淆导致很多错误的架构决策。我的判断标准是控制流在谁手里

维度工作流(Workflow)Agent
控制流代码里写死,LLM 只在节点内做处理LLM 决定下一步做什么
步数可预测,路径有限不可预测,可能循环
可测试性高,固定的输入输出低,非确定性
单次成本可控(节点数固定)方差大
适合的场景流程已知、要求稳定路径未知、需要探索

举个具体例子。我们的「发布后健康检查」是工作流:

// 流程固定,只有 5 步,不需要 LLM 决定下一步
public HealthCheckResult checkAfterDeploy(DeployContext ctx) {
    var metrics = collectMetrics(ctx.service(), Duration.ofMinutes(10));
    var errors  = collectErrorLogs(ctx.service(), Duration.ofMinutes(10));
    var traces  = sampleTraces(ctx.service(), 100);

    var analysis = llm.analyze(CHECK_PROMPT, metrics, errors, traces);  // LLM 只在这一个节点
    boolean healthy = analysis.score() > 0.7;

    if (!healthy) rollbackService.rollback(ctx);    // 回滚逻辑是确定性的
    notify(ctx.owner(), analysis);
    return result;
}

这里 LLM 只负责「分析」这一个环节,整个流程的骨架是代码控制的。这不算 Agent,但它能用,而且非常稳定——跑了 14 个月,零误回滚。

而「排查一个未知原因的故障」就是 Agent 场景:不知道要看哪个服务、要查几个指标、查到什么程度就该停。路径完全依赖中间结果,必须由模型动态决定。

Agent 的核心循环:感知-规划-行动

我们实现的循环结构是这样的:

public class AgentLoop {

    private static final int MAX_STEPS = 25;

    public TaskResult run(String task, ToolRegistry tools, Memory memory) {
        List<Step> history = new ArrayList<>();

        for (int step = 0; step < MAX_STEPS; step++) {
            // ── 1. 感知:组装当前可见的世界状态 ──
            Context ctx = Context.builder()
                    .systemPrompt(systemPrompt)
                    .tools(tools.specifications())
                    .episodicMemory(memory.recall(task))      // 召回相关经验
                    .history(history)
                    .build();

            // ── 2. 规划+决策:模型输出下一步 ──
            ModelResponse resp = model.call(ctx);
            history.add(Step.of(resp));

            if (resp.isFinalAnswer()) {
                return finish(history, resp.answer());
            }

            // ── 3. 行动:执行工具 ──
            ToolCall call = resp.toolCall();
            ToolResult result = tools.execute(call);           // 含权限校验、沙箱

            // ── 4. 观察:把结果放回历史 ──
            history.add(Step.of(call, result));

            // ── 5. 收敛判断 ──
            if (shouldStop(history)) {
                return abort(history, stopReason(history));
            }
        }
        return abort(history, "达到最大步数");
    }
}

五个环节里,最容易被写坏的是第 1 步(感知)和第 5 步(收敛)。

感知:上下文组装是有策略的

「把历史全塞进去」是最差的做法。我们在前面几篇里详细写过上下文工程,这里只说和循环结构相关的部分——不同阶段该看到的东西不一样

Context buildContext(Phase phase, List<Step> history) {
    return switch (phase) {
        // 规划阶段:不需要看到所有历史细节,看到目标和已有结论即可
        case PLAN -> ctx(systemPrompt, tools, historySummary(history));

        // 行动阶段:需要最近几步的完整细节(工具返回的原文)
        case ACT -> ctx(systemPrompt, tools, recentSteps(history, 3),
                        historySummary(history));

        // 总结阶段:需要全部结论,不需要工具返回的原始 JSON
        case SUMMARIZE -> ctx(systemPrompt, allConclusions(history));
    };
}

这个设计让单任务的平均 token 从 41,000 降到 26,800,而且准确率还提升了 4 个百分点(噪音少了)。

收敛:最难的部分

Agent 最大的风险不是做错,是停不下来。我们统计过没有收敛机制时的步数分布:

步数区间占比平均成本
1~5 步41%¥0.21
6~15 步38%¥0.58
16~30 步14%¥1.34
>30 步(含死循环)7%¥4.72

7% 的任务消耗了 34% 的成本,其中真正死循环的(在两个工具之间反复横跳)占 2.3%。

我们的收敛判断是四重保险:

boolean shouldStop(List<Step> history) {
    // 1. 硬上限
    if (history.size() >= 25) return true;

    // 2. 成本上限(不只是步数)
    if (totalTokens(history) > 80_000) return true;

    // 3. 循环检测:最近 4 步的签名重复
    if (hasRepeatingPattern(history, window = 4, minRepeat = 2)) return true;

    // 4. 无进展检测:连续 3 步没有获得新信息
    if (noNewInformation(history, 3)) return true;

    return false;
}

// 第 3 条的实现
boolean hasRepeatingPattern(List<Step> h, int window, int minRepeat) {
    if (h.size() < window * minRepeat) return false;
    List<String> sigs = h.stream().map(Step::signature).toList();   // 工具名+参数规范化
    List<String> tail = sigs.subList(sigs.size() - window * minRepeat, sigs.size());
    for (int i = 1; i < minRepeat; i++) {
        if (!tail.subList(0, window)
                 .equals(tail.subList(i * window, (i + 1) * window))) {
            return false;
        }
    }
    return true;
}

第 4 条「无进展检测」是我们后来加的,也是最有效的。判断方法是看工具返回的内容和之前某一步是否高度相似(相似度 > 0.92)。它能抓到那些「换了个工具但得到相同信息」的情况——循环检测抓不到这类,因为工具名不一样。

加上这四重之后,>30 步的任务从 7% 降到 0.9%,成本下降 31%。

什么场景我们改回了工作流

这才是这篇最想说的部分。改造上线三个月后,我们统计了各场景的表现:

场景Agent 成功率工作流成功率平均成本决定
未知原因故障排查78%34%¥0.91保留 Agent
容量评估与扩容建议83%71%¥0.64保留 Agent
发布后健康检查94%97%¥0.38改回工作流
告警噪音分类91%93%¥0.07改回工作流
日志关键词告警响应89%96%¥0.22改回工作流

改回去的三个场景有个共同特征:处理路径是固定的,不需要探索

以「日志关键词告警响应」为例,流程永远是:拿到告警 → 拉取上下文日志 → 判断是否需要人工 → 通知。Agent 版本虽然成功率也有 89%,但它偶尔会「自作主张」多查几个指标,单次成本从 0.06 元涨到 0.22 元,而且延迟从 3 秒涨到 19 秒。对一个每天触发 400 次的告警来说,这个代价不值得。

告警噪音分类那个更典型。它的判断逻辑其实很清晰(基于规则 + 一个简单分类),Agent 版本反而在边界 case 上更容易出错——因为模型会「推理」出一个看似合理但不符合我们内部约定的分类。工作流版本直接查分类表,100% 一致。

还有一个隐性成本:Agent 的可解释性差。告警分类错了要复盘,工作流能明确说是哪条规则命中的;Agent 版本只能说「模型这么判断的」,没法改进。

我们的混合架构

最后定下来的架构是这样:

                    ┌─────────────────┐
                    │   事件入口       │
                    │ (告警/定时/人工) │
                    └────────┬────────┘
                             ▼
                    ┌─────────────────┐
                    │   路由器         │  ← 规则 + 小模型分类
                    │ 决定用哪种模式    │
                    └────┬───────┬────┘
                         │       │
              已知流程 ───┘       └─── 未知问题
                   ▼                   ▼
        ┌──────────────────┐  ┌──────────────────┐
        │  工作流引擎        │  │   Agent 循环      │
        │  - 固定步骤        │  │  - 感知/规划/行动  │
        │  - 确定性执行      │  │  - 工具调用       │
        │  - LLM 只做节点处理 │  │  - 记忆系统       │
        └──────────────────┘  └──────────────────┘
                   │                   │
                   └─────────┬─────────┘
                             ▼
                    ┌─────────────────┐
                    │  统一的动作执行层  │  ← 权限、沙箱、审计
                    └─────────────────┘

关键设计有两个。

一是路由器。它决定一个任务走工作流还是 Agent,判断用规则 + 一个很便宜的小模型(qwen-turbo,单次 0.0003 元)。规则覆盖 80% 的情况(告警类型 → 处理方式是有映射表的),剩余的用模型分类。路由错误率 3.2%,可以接受——因为路由错了最多是效果差一点,不会出错。

二是统一的动作执行层。不管是工作流还是 Agent 调用工具,都走同一套权限校验、沙箱执行、审计日志。这一点非常重要,否则 Agent 会成为一个绕过既有安全体系的后门。我们在上一版架构里犯过这个错——Agent 有自己的一套工具调用代码,权限校验是单独实现的,后来发现两边规则不一致。

工具设计:Agent 和工作流的需求不一样

改造过程中我们发现,同一个能力给工作流用和给 Agent 用,接口设计应该不同。

给工作流的工具追求精确——参数明确、返回结构固定、一次调用拿全所有信息:

// 工作流用:一次拿全,参数明确
public PodDiagnosis diagnosePod(String namespace, String podName,
                                Duration window, boolean includeEvents) {
    ...
}

给 Agent 的工具追求可探索和容错——参数宽松、返回信息密度高、错误信息要有指导性:

// Agent 用:参数可省略,返回结果自带下一步建议
@Tool(description = """
    诊断一个 Pod 的异常状态。如果不确定 Pod 名,先用 list_pods 查询。
    返回:状态、事件、最近日志摘要、资源使用率、以及初步判断。
    """)
public PodDiagnosis diagnosePod(
        @ToolParam(description = "Pod 名,支持前缀匹配", required = true)
        String podName,
        @ToolParam(description = "命名空间,不传则搜索所有有权限的命名空间", required = false)
        String namespace,
        @ToolParam(description = "日志时间窗口,单位分钟,默认 30", required = false)
        Integer windowMinutes) {

    List<Pod> matches = k8s.findPods(podName, namespace);
    if (matches.isEmpty()) {
        // 错误信息要能指导模型调整行为
        return PodDiagnosis.notFound(podName,
            "未找到匹配的 Pod。可用 list_pods 查看当前所有 Pod。");
    }
    if (matches.size() > 1) {
        return PodDiagnosis.ambiguous(matches.stream().map(Pod::name).toList(),
            "匹配到多个 Pod,请用完整名称。");
    }
    ...
}

「匹配到多个时返回候选列表」这个设计,看起来很小,但对 Agent 的成功率影响很大。以前工具直接抛异常,Agent 只能猜;现在它拿到了候选,下一步就能选对。我们把这种「可恢复的错误」做成了工具设计的规范。

成本和效果的最终数字

混合架构上线两个月,对比纯 Agent 版本:

指标纯 Agent混合架构
日均任务量1,2401,390
整体成功率84.2%89.7%
日均成本¥682¥341
P50 耗时34s11s
P99 耗时186s92s
因超时放弃的任务4.1%1.3%

成本降了一半,成功率反而提升——因为那些本来就不该用 Agent 的简单任务,用工作流又快又准。

几条判断标准

如果你也在纠结某个场景该用 Agent 还是工作流,我们内部用的检查清单:

  1. 能不能提前写出完整的步骤?能写出来 → 工作流。写不出来(因为依赖中间结果)→ Agent。
  2. 出错的代价有多大?代价高(涉及资金、数据删除、对外承诺)→ 工作流,或者 Agent + 强制人工确认。
  3. 调用频率高吗?每天几百次以上的固定任务 → 工作流,成本差是数量级的。
  4. 需要可解释吗?事后要复盘归因的 → 工作流。Agent 的决策过程记录得再全,也不如一条明确的规则好解释。
  5. 输入的变化性大吗?用户自由提问、问题形态千变万化 → Agent。输入是结构化事件(告警、定时任务)→ 大概率工作流。

按我们的经验,一个中等规模的 AI 系统里,大概 60~70% 的场景适合工作流,30~40% 需要 Agent。一开始就全上 Agent,会付出不必要的成本和稳定性代价。

就写到这。如果哪天你也被《AI Agent 架构设计:从 ChatBot 到自主智能体》里同一个坑绊住,回来翻这篇,能省半小时。

参考