今年4月我开始做一个面向制造业 MES 领域的业务分析 Agent。用户可以用自然语言问「工单 WO-xxx 什么状态」、「8D 报告 D4 怎么做」,Agent 会从知识库或 MES 系统返回答案。
项目跑通之后,我以为最大的挑战是 Prompt 调优和 RAG 准确率。结果第一周就被打脸——**响应延迟 55 秒**。
用户点一下问题,盯着空白屏幕 55 秒,体验非常糟糕。更离谱的是,我一开始以为是 LLM 推理慢,差点去换更快的模型。
后来发现 96% 的时间根本没花在 LLM 上。
这篇文章记录我用 Trace 工具定位真正瓶颈、把延迟压到 1 秒的完整过程。
一、先别猜,用数据定位
遇到性能问题,最忌讳的就是猜。
- 是不是模型太慢?
- 是不是 Prompt 太长?
- 是不是网络问题?
这些猜测会让你做出错误的优化决策——比如花三天换模型,结果发现延迟一点没变。
我用 LangSmith 的 Trace 功能,把一次请求里每个节点的耗时拆解出来。
结果长这样(简化版):
planner_node 0.82s ← LLM 调用
tool_decide_node 0.71s ← LLM 调用
tool_exec_node 57.57s ← ⚠️ 问题在这里
task_execute_node 3.69s ← LLM 调用
reflect_node 1.12s ← LLM 调用
summary_node 2.14s ← LLM 调用
─────────────────────────────
总计 约 66s
tool_exec_node 一个人吃掉了 57 秒,占了 86% 的时间。
而其他 5 个节点的 LLM 调用加起来才 8.5 秒——问题根本不是 LLM 慢。
二、定位到具体动作:模型冷加载
tool_exec_node 里做了三件事:
- 加载 BGE Embedding 模型(约 25M 参数)
- 加载 Reranker 模型(
bge-reranker-v2-m3,约 568M 参数) - 执行向量检索 + Rerank 精排
加一行 print 实测:
【懒加载】首次调用,加载 BGE Embedding 和 Chroma 向量库...
Loading weights: 100%|██████| 71/71
【懒加载】加载 Reranker 模型 bge-reranker-v2-m3...
Loading weights: 100%|██████| 393/393
【懒加载】Reranker 加载完成
(后续检索 + 精排)
模型加载本身占了大头——约 50 秒。
根因找到了: 代码用了「懒加载」——第一次调用时加载模型到内存。听起来很聪明,实际上每次进程重启都要重新加载 50 秒。
更糟糕的是,我最初在 Streamlit 里跑这个 Agent——Streamlit 每次交互都 rerun 整个脚本,导致每次点击问题都要重新加载模型。
这就是 55 秒的真相:不是算法慢,是架构问题。
三、三个优化动作
动作 1:模型预热
把加载从「用户请求时懒加载」挪到「服务启动时预热」。 def _warmup_models(): """服务启动时预热,用户请求全部享受稳态性能""" print("【模块预热】开始加载模型...") _ = _get_vector_db() # 加载 Embedding + Chroma _ = get_reranker() # 加载 Reranker _ = chroma_search("预热查询") # 真跑一次,吃掉 JIT 编译开销 print("【模块预热】完成")
模块 import 时自动触发
_warmup_models() 效果:
- 服务启动慢 50 秒(一次性,运维等)
- 用户请求全部享受稳态性能
动作 2:双进程架构
Streamlit 不适合做重计算。 改成 FastAPI 后端 + Streamlit 前端双进程架构:
| 服务 | 职责 | 启动速度 | 特点 |
|---|---|---|---|
| Streamlit(前端) | UI 渲染、登录、对话交互 | 秒开 | 不加载任何模型 |
| FastAPI(后端) | Agent 编排、模型推理 | 50s 预热 | 模型常驻内存 |
数据流:用户浏览器 → Streamlit (8501) → HTTP → FastAPI (8000) → 返回结果
模型加载隔离到 FastAPI 常驻进程,Streamlit 重启不影响。
动作 3:快速模式跳过 Reranker
这一步是质变。
Reranker(bge-reranker-v2-m3)是一个 568M 参数的大模型,作用是"把粗召回的 10 篇文档精排到 Top 3"。
问题在于——它在 CPU 上跑得极慢。 每次精排 10 对文档要 30 秒。
但我实测发现:向量 Top-2 的精度对 90% 的查询已经够用。
于是我做了个决策:快速模式直接跳过 Reranker。
深度模式:完整 RAG
docs = vector_db.similarity_search(query, k=10) # 粗召回 10 条 scores = reranker.compute_score(pairs) # 精排(慢,30s) top_docs = ranked[:3]
快速模式:直接 Top-2
docs = vector_db.similarity_search(query, k=2) # 直接取 2 条
不做 Reranker
代价是 RAG 召回精度略降(从 1.0 降到 0.7~0.8),但 Top-2 仍准确。
36 秒 → 1 秒。
四、最终效果
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 快速模式响应 | 55s | 1.06s |
| 首字延迟 | — | 0.74s |
| 深度模式响应 | 55s | 44s |
| 缓存命中时 | — | 0.01s |
优化幅度 98%。
而且我保留了深度模式(完整 Reranker + 六节点 Reflection),用于复杂分析任务——简单问题快速,复杂分析深度。
五、几条反思
1. 定位比调参重要 100 倍
如果我一开始就去"换更快的 LLM",可能会花三天,性能一点没变——因为 LLM 只占总耗时的 14% 。
先用工具剖分细节,再动手优化。
2. 懒加载不一定好
"懒加载"听起来优雅——按需加载,不浪费时间。但实际上:
- 服务端场景:流量稳定,早晚要加载,不如预热
- 用户请求时加载:每次都要等一次,体验极差
判断懒加载好坏的标准是:这个"第一次"多久发生一次? 只发生一次(服务启动)→ 预热更好;经常发生(不同用户不同模型)→ 懒加载更好。
3. 架构问题比代码问题更隐蔽
我花了两天才意识到"Streamlit rerun 导致模型重加载"这个问题——因为代码逻辑本身是对的,只是 Streamlit 的执行模型和它不兼容。
同样的代码,换个执行环境可能完全不一样。
4. 不要追求"完美",要追求"合适"
Reranker 确实能提升精度。但对于一个快速问答场景,1 秒响应比 0.2 的精度提升更重要。
所以我保留了两种模式——用户根据场景选择。
六、延伸:这个项目后来做了什么
性能优化只是开始。后来项目还做了不少工程化改造:
- BOM 版本守卫:用代码级前置校验解决 LLM 幻觉(比 Prompt 规则更可靠)
- MES 实时数据集成:封装 6 个工具(工单/库存/设备/质量),让 Agent 能查实时数据
- JWT 认证 + 多会话隔离:从"个人 Demo"到"能交付"
- Prometheus + Grafana:6 个业务指标实时监控
- Docker Compose 容器化:5 容器一键启动
- 单元测试 + CI/CD:15 个测试 + GitHub Actions
项目开源在 GitHub:
https://github.com/Wang-qi-git/langgraph-mes-business-agent
感兴趣的同学可以看看,欢迎交流。
最后
这个项目让我最深的体会是:做 AI 应用,工程能力比算法能力更重要。
Prompt 调得再好、模型选得再对,如果架构是错的,用户体验依然是灾难。
55 秒到 1 秒,靠的不是更聪明的模型,而是更清醒的架构判断。