企业级 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 |
| Embedding | BGE-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-002 | 1536 | 8K | 一般 | API | ¥0.8/K tokens |
| BGE-large-zh | 1024 | 512 | 强 | 本地 | 一次性 GPU 成本 |
| BGE-M3 | 1024 | 8K | 强 | 本地 | 一次性 GPU 成本 |
我们选择 BGE-Large-ZH:
- MTEB 中文榜单排名靠前
- 512 维上下文够用(知识块控制在 500 字以内)
- 本地部署,数据不出境
成本节省:从 OpenAI API 降到本地部署,月省 ¥3000+。
四、向量库:Milvus,规模决定选择
| 维度 | Milvus | PGVector |
|---|---|---|
| 性能 | 专为向量设计,毫秒级 | 基于 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 调用 | ¥1500 | Kimi 为主,DeepSeek 备用 |
| 向量库存储 | ¥200 | 华为云 Milvus |
| 服务器资源 | ¥500 | 2C4G × 2 台 |
| 总计 | ~¥2200 |
优化策略:
- 消息压缩:Tool 结果超长时截断(List 只保留前 20 条,String 超 4000 字符截断)
- 结果缓存:高频 Query 缓存 5 分钟
- 模型分级:简单任务用小模型(规划中)
九、可观测性:没有监控就是瞎子
必须监控的指标:
- 响应延迟(P50 / P95 / P99)
- Token 消耗(单次 / 日均)
- Tool 调用成功率
- 模型切换次数(主模型故障频率)
方案:
- Prometheus + Grafana 基础监控
- 钉钉私信告警(模型切换、服务故障)
- 数据库表记录 LLM 调用日志(便于事后分析)
十、团队匹配:技术选型要考虑人
再先进的技术,团队 hold 不住也是白搭。
我们的团队技术储备:
- Java 为主力开发语言(Agent 核心、权限、Tool 管理)
- Python 负责 AI 相关服务(LLM 调用、向量检索、质检)
- 部署侧使用 K8s
选型逻辑:选择与团队技术储备匹配的组件,降低认知负担和运维复杂度。不对标的工具链再好,也得考虑团队上手成本。
总结:10 个决策背后的共同原则
- 明确需求:规模多大?预算多少?团队什么水平?
- 务实优先:不追新,不造轮子,够用就好
- 预留扩展:为未来增长留空间,但不过度设计
- 降级比优化更重要:宁可提示不可用,不能卡死
总成本 ¥2200/月,支撑了一个企业级 Agent 系统。这个数字不是炫耀,是告诉你:小团队也能做 AI,关键是用对工具、算清账。
下篇预告
《知识库质检方案:三级 Gate 如何拦截垃圾知识》
- 自动检查:置信度评分与矛盾检测
- 人工审核:运营确认的边界情况处理
- 降级策略:Python 服务超时自动放行
这是系列四部曲的第二篇,讲知识入库前的"质量关卡"。
标签:#Java #AI #Agent #技术选型 #企业落地 #后端开发 #架构设计
如果这个系列对你有帮助,欢迎 👍 点赞 | 🔖 收藏 | 💬 评论。我会持续更新这个系列,下篇讲知识库质检机制。