企业级 Agent 自研复盘:真实成本 ¥2200/月

0 阅读11分钟

企业级 Agent 自研复盘:真实成本 ¥2200/月

5 个月,从 Demo 到生产环境,我们做了 10 个关键决策。总成本 ¥2200/月,这篇文章把账算清楚。


📖 系列阅读地图

序号文章内容状态
本文技术选型决策:我们为什么自研、为什么选这些组件新发布
《知识库质检方案》入库前的质量 Gate:三级审核机制新发布
《ReAct Agent 从零实现》Java 构建企业级 Agent 的完整架构已发布
《RAG 准确率三级跳》检索优化:30%→87% 的完整过程已发布

建议按顺序阅读。本文是系列的前传,讲"为什么我们选择了这条路"。


引言:30 秒说清我们在做什么

"我们自研了一套 AI Agent 系统,核心是让 AI 能调用公司内部的业务接口。用户用自然语言提问,AI 自动判断需要调用哪些接口、组合数据后给出答案。"

比如问"哪些设备需要补货",AI 会自动查销售数据、库存数据、设备容量,然后计算出补货建议。

但把这个从 Demo 做到稳定运行在生产环境,我们花了 5 个月。这篇文章不是 LangChain 的使用教程,是 5 个月里 10 个关键决策的真实复盘——每个决策背后都有凌晨的告警、线上的 Bug、和"如果当初选了另一条路会怎样"的假设。

先给结论:

维度我们的选择月成本
框架自研(Java)-
主模型Kimi~¥1500
备用模型DeepSeek~¥200
EmbeddingBGE-Large-ZH(本地)-
向量库Milvus(华为云)~¥200
服务器2C4G × 2 台~¥500
总计~¥2200

相比单用 GPT-4(¥8000+/月),成本降低了 70%。怎么做到的?往下看。


一、框架选型:为什么放弃 LangChain,选择自研

LangChain 的优势很明显:30 分钟搭出 Demo,生态丰富,文档完善。但我们最终选择了自研。三个原因。

原因 1:黑盒问题

// LangChain 的调用链
chain.invoke(input) 
// 里面发生了什么?不知道。
// 出问题的时候,你根本不知道从哪里开始排查

真实案例:上线第一周,凌晨 2 点钉钉告警——Agent 回复了错误数据。排查 2 小时,最后发现是消息截断策略把 tool_calls 和 tool result 的对应关系破坏了,LLM API 报 400,错误信息却极其模糊。

这类问题在自研代码里 10 分钟就能定位,在封装框架里可能需要 2 小时。

原因 2:流式降级的坑

我们用了双模型网关(Kimi 主 + DeepSeek 备)。非流式场景下,主模型失败抛异常 → catch → 切备用,逻辑很清晰。

流式场景下,Kimi 返回 HTTP 429 错误时不会抛出异常,而是调用 callback.onError() 然后静默返回。我们最初在 catch 块里写降级逻辑,结果这段代码从未被执行过

修复方案是在 callback 层包一层 wrappedCallback:若 onData 从未被调用(hasData=false),才触发降级。这个 Bug 在 2026-03-26 的凌晨被发现,当天修复。

/** 流式降级:用 AtomicBoolean 追踪状态,避免 mutable anonymous class */
public class FallbackStreamingCallback implements StreamingCallback {
    
    private final AtomicBoolean hasData = new AtomicBoolean(false);
    private final LLMRequest request;
    private final StreamingCallback delegate;
    private final FallbackStrategy fallback;
    
    @Override
    public void onData(String chunk) {
        hasData.set(true);
        delegate.onData(chunk);
    }
    
    @Override
    public void onError(Throwable error) {
        Optional.of(hasData.get())
            .filter(Boolean::booleanValue)
            .map(__ -> this::propagateError)
            .orElse(this::executeFallback)
            .accept(error);
    }
    
    private void executeFallback(Throwable error) {
        log.warn("流式调用零数据失败,触发降级: {}", error.getMessage());
        fallback.stream(request, delegate);
    }
    
    private void propagateError(Throwable error) {
        delegate.onError(error); // 数据已在流出,不可回退
    }
}

这个教训告诉我们:流式 LLM Provider 通过 callback 报错 ≠ 抛出异常,封装框架的异常处理对此类错误可能完全无效。

原因 3:定制需求与框架抽象的冲突

我们需要在 ReAct 循环中加入:

  • 配额检查(每轮都要查用户余额)
  • 敏感词过滤(每轮都要检查)
  • 多租户上下文传递
  • 写操作权限校验 + 二次确认

LangChain 的 AgentExecutor 封装让这种定制很痛苦。而自研的循环,每一行都可控:

public class ReActAgentLoop {
    
    private final List<LoopInterceptor> interceptors;
    
    public String run(ChatRequest req, SessionHistory history) {
        return Stream.iterate(
                new LoopContext(req, history, 0),
                ctx -> ctx.iteration() < MAX_ITERATIONS,
                ctx -> ctx.next(roundExec.execute(ctx))
            )
            .filter(LoopContext::hasFinalAnswer)
            .findFirst()
            .map(LoopContext::finalAnswer)
            .orElseThrow(() -> new AgentTimeoutException("超过最大迭代次数"));
    }
}

/** 每一轮循环的上下文,不可变,保证线程安全 */
public record LoopContext(
    ChatRequest request,
    SessionHistory history,
    int iteration,
    Optional<String> finalAnswer
) {
    LoopContext next(ToolResults results) {
        return new LoopContext(request, history.append(results), iteration + 1, finalAnswer);
    }
    boolean hasFinalAnswer() { return finalAnswer.isPresent(); }
}

/** 拦截器链:配额 → 截断 → 模型调用 → 权限 → 工具执行 → 压缩 */
public interface LoopInterceptor {
    void intercept(LoopContext ctx, Chain chain);
}

决策建议

  • 快速验证 / MVP → LangChain 够用
  • 生产环境 / 复杂业务 → 建议自研或轻量封装

二、模型选型:Kimi + DeepSeek 的真实成本账

为什么选 Kimi 做主模型

  • 32K 上下文,适合长对话
  • 工具调用稳定(Function Calling 格式规范)
  • 国内 API,延迟低、合规

为什么选 DeepSeek 做备用(不是 GPT-4)

我们对比过三个方案:

方案月成本稳定性数据安全工具调用
GPT-4 单模型¥8000+数据出境 ❌稳定
Claude 单模型¥6000+数据出境 ❌稳定
Kimi + DeepSeek~¥2000本地合规 ✅稳定

DeepSeek 选型的一个坑:我们最初想用 deepseek-reasoner(推理模型)做备用,结果发现它响应慢、token 贵、工具调用不稳定。最终改用 deepseek-chat(DeepSeek-V3),工具调用稳、速度快、约 ¥1/M token,这才是合格的备用模型

双模型网关实现

/** 模型网关:用函数式组合消除 try-catch,主备自动切换 */
@Component
@RequiredArgsConstructor
public class ModelGateway {
    
    private final LLMProvider primary;
    private final LLMProvider fallback;
    private final CircuitBreakerRegistry cbRegistry;
    private final ModelSwitchAlarm alarm;
    
    public String chat(List<Message> messages) {
        return primary.withCircuitBreaker(cbRegistry)
            .apply(messages)
            .recover(this::switchToFallback)
            .getOrElseThrow(() -> new LLMUnavailableException("主备均不可用"));
    }
    
    private String switchToFallback(Throwable error, List<Message> messages) {
        alarm.notify(new ModelSwitchEvent(error, primary.name()));
        return fallback.chat(messages);
    }
}

/** LLM Provider 统一抽象,Kimi 和 DeepSeek 分别实现 */
public interface LLMProvider {
    String name();
    String chat(List<Message> messages);
    
    default Try<String> withCircuitBreaker(CircuitBreakerRegistry cb) {
        return Try.ofSupplier(
            CircuitBreaker.decorateSupplier(
                cb.circuitBreaker(name()), 
                () -> chat(messages)
            )
        );
    }
}

熔断参数(供参考):

resilience4j:
  circuitbreaker:
    instances:
      llm:
        failure-rate-threshold: 50
        wait-duration-in-open-state: 30s
        sliding-window-size: 10

踩坑:主模型失败时告警不能发到钉钉群——运营群不应该看到这些内部技术告警。我们改为发私信给 admin。


三、Embedding 模型:BGE-Large-ZH,中文场景的最优解

模型维度上下文中文能力部署成本
text-embedding-ada-00215368K一般API¥0.8/K tokens
BGE-large-zh1024512本地一次性 GPU 成本
BGE-M310248K本地一次性 GPU 成本

我们选择 BGE-Large-ZH

  • MTEB 中文榜单排名靠前
  • 512 维上下文够用(知识块控制在 500 字以内)
  • 本地部署,数据不出境

成本节省:从 OpenAI API 降到本地部署,月省 ¥3000+


四、向量库:Milvus,规模决定选择

维度MilvusPGVector
性能专为向量设计,毫秒级基于 PG,性能一般
规模支持十亿级适合千万级以下
功能分布式、多索引类型简单,够用
运维需要额外维护现有 PG 基础设施
成本需独立资源零额外成本

我们选择 Milvus(华为云托管版):

  • 预计向量规模百万级(145 页知识库,每页分 5-10 块)
  • 需要混合检索(向量 + 关键词)
  • 未来可能扩展到多租户场景

but,如果是小项目,我推荐 PGVector。零运维成本,SQL 直接查,对大多数应用够用。


五、记忆设计:三层架构,每层都踩过坑

记忆架构:三层记忆系统设计

坑 1:消息历史爆炸

最初没做截断,用户聊 20 轮后,context 超过 32K,LLM 报错。我们做了三步截断策略:

/** 消息截断:职责链模式,每个 TruncationRule 只做一件事 */
@Component
@RequiredArgsConstructor
public class MessageHistoryManager {
    
    private final List<TruncationRule> rules = List.of(
        new CountLimitRule(30),           // 保留最近 30 条
        new TotalLengthRule(8000),        // 总字符不超过 8000
        new OrphanToolMessageRule()       // 修复孤立 tool 消息
    );
    
    public List<Message> truncate(List<Message> history) {
        return rules.stream()
            .reduce(history, (msgs, rule) -> rule.apply(msgs), (a, b) -> b);
    }
}

public interface TruncationRule {
    List<Message> apply(List<Message> messages);
}

public class OrphanToolMessageRule implements TruncationRule {
    @Override
    public List<Message> apply(List<Message> messages) {
        Set<String> validCallIds = messages.stream()
            .filter(m -> m.role() == ASSISTANT)
            .flatMap(m -> m.toolCalls().stream())
            .map(ToolCall::id)
            .collect(Collectors.toSet());
        
        return messages.stream()
            .filter(m -> !isOrphanedToolResult(m, validCallIds))
            .toList();
    }
}

坑 2-3:孤立 tool 消息和 Kimi assistant 格式导致的 400 错误,在系列第三篇《ReAct Agent 从零实现》中有完整复盘,这里不再重复。这篇里你只看到我最终用 OrphanToolMessageRule 职责链做了封装,背后的血泪史在那篇文章里。

坑 4:两个 Service 模型默认值不一致

StreamingAgentServiceImpl(PC 端)和 AgentServiceImpl(钉钉端)的模型默认值不同,导致两端返回结果质量差异大。

修复:统一默认值为 kimi-k2-0711-preview


六、容错设计:降级比优化更重要

生产环境必须考虑的降级策略:

场景降级方案
LLM 超时3 秒连接超时,15 秒读取超时,超时切备用模型
向量库超时重试 3 次,失败则降级到数据库关键词查询
Tool 失败记录日志,返回错误信息给 LLM,让 LLM 决定如何回复
Python 质检服务挂了直接放行,不阻断主流程(见系列第二篇)

核心原则:宁可给出"服务暂时不可用"的提示,也不能让用户请求卡死。

OkHttpClient 超时配置

this.httpClient = new OkHttpClient.Builder()
        .connectTimeout(15, TimeUnit.SECONDS)
        .readTimeout(90, TimeUnit.SECONDS)   // LLM 推理标准配置,不得低于 60s
        .writeTimeout(30, TimeUnit.SECONDS)
        .build();

这个踩坑的完整排查过程在系列第三篇《ReAct Agent 从零实现》里有详细记录。核心教训:凡是调用 LLM / 第三方 AI API 的客户端,readTimeout 不得低于 60s,建议 90s。


七、权限与安全:写操作必须二次确认

设计原则:

  • 读操作(查询数据):开放给所有授权用户
  • 写操作(修改配置):必须角色校验 + 二次确认
/** 工具执行管线:用注解驱动替代 if/else 嵌套 */
@Component
public class ToolExecutionPipeline {
    
    private final ToolRegistry registry;
    private final List<ToolExecutionFilter> filters;
    
    public ToolResult execute(ToolCall call, UserContext user) {
        AgentTool tool = registry.resolve(call.name());
        
        return filters.stream()
            .reduce(ToolResult.identity(),
                (result, filter) -> filter.apply(tool, call, user),
                (a, b) -> b)
            .orElseGet(() -> tool.invoke(call.arguments()));
    }
}

/** 写操作权限拦截:通过 @RequireAdmin 注解声明,AOP 统一处理 */
@Aspect
@Component
public class WritePermissionAspect {
    
    @Around("@annotation(RequireAdmin) && args(call, user)")
    public Object checkAdmin(ProceedingJoinPoint jp, ToolCall call, UserContext user) {
        return user.hasRole(ADMIN) 
            ? proceedWithAudit(jp, call, user)
            : ToolResult.denied("需要管理员权限");
    }
    
    private Object proceedWithAudit(ProceedingJoinPoint jp, ToolCall call, UserContext user) {
        return isHighRisk(call) 
            ? confirmationService.request(call, user)  // 钉钉卡片二次确认
            : jp.proceed();
    }
}

八、成本控制:Token 消耗是大头

成本结构(月均):

项目金额说明
LLM 调用¥1500Kimi 为主,DeepSeek 备用
向量库存储¥200华为云 Milvus
服务器资源¥5002C4G × 2 台
总计~¥2200

优化策略:

  1. 消息压缩:Tool 结果超长时截断(List 只保留前 20 条,String 超 4000 字符截断)
  2. 结果缓存:高频 Query 缓存 5 分钟
  3. 模型分级:简单任务用小模型(规划中)

九、可观测性:没有监控就是瞎子

必须监控的指标:

  • 响应延迟(P50 / P95 / P99)
  • Token 消耗(单次 / 日均)
  • Tool 调用成功率
  • 模型切换次数(主模型故障频率)

方案:

  • Prometheus + Grafana 基础监控
  • 钉钉私信告警(模型切换、服务故障)
  • 数据库表记录 LLM 调用日志(便于事后分析)

十、团队匹配:技术选型要考虑人

再先进的技术,团队 hold 不住也是白搭

我们的团队技术储备:

  • Java 为主力开发语言(Agent 核心、权限、Tool 管理)
  • Python 负责 AI 相关服务(LLM 调用、向量检索、质检)
  • 部署侧使用 K8s

选型逻辑:选择与团队技术储备匹配的组件,降低认知负担和运维复杂度。不对标的工具链再好,也得考虑团队上手成本。


总结:10 个决策背后的共同原则

  1. 明确需求:规模多大?预算多少?团队什么水平?
  2. 务实优先:不追新,不造轮子,够用就好
  3. 预留扩展:为未来增长留空间,但不过度设计
  4. 降级比优化更重要:宁可提示不可用,不能卡死

总成本 ¥2200/月,支撑了一个企业级 Agent 系统。这个数字不是炫耀,是告诉你:小团队也能做 AI,关键是用对工具、算清账。


下篇预告

《知识库质检方案:三级 Gate 如何拦截垃圾知识》

  • 自动检查:置信度评分与矛盾检测
  • 人工审核:运营确认的边界情况处理
  • 降级策略:Python 服务超时自动放行

这是系列四部曲的第二篇,讲知识入库前的"质量关卡"。


标签#Java #AI #Agent #技术选型 #企业落地 #后端开发 #架构设计

如果这个系列对你有帮助,欢迎 👍 点赞 | 🔖 收藏 | 💬 评论。我会持续更新这个系列,下篇讲知识库质检机制。