很多团队引入大模型后,第一个遇到的问题都一样:回答不准、不可追溯、追问一个字都丢失上下文。这篇文章分享我们在危险货物运输行业标准问答场景中,从踩坑到解决的真实技术历程。
一、单靠大模型,专业场景根本不够用
大模型的参数化记忆天然有两个无法绕过的缺陷:
- 领域标准知识过时
:GB、DB、JT/T 系列标准每隔几年就修订一版,模型训练数据追不上
- 幻觉无法消除
:涉及具体数值、限值、条款编号时,模型"合理编造"的概率极高
解法不是换更大的模型,而是给模型一个可信赖的知识底座。
二、RAG 层:混合检索 + 三阶段精排
我们的检索层基于 ES(Elasticsearch) 构建,采用向量检索(KNN)+ BM25 全文检索的混合召回策略。
2.1 两路召回,各取所长
用户问题
├── 向量检索(KNN)─── 语义相似,找"说同一件事"的 chunk
└── BM25 全文检索 ─── 关键词命中,找"用了同样词"的 chunk
↓
结果合并 → Rerank 重排序(bge-reranker-large)
↓
Top-K 高质量 chunk → 注入 LLM Prompt
向量检索擅长捕捉语义,BM25 擅长精确命中专业术语(如 GB/T30685、JT/T617)。两路互补,缺一不可。
2.2 Rerank:不是所有召回都值得用
召回是粗筛,Rerank 才是精选。我们使用 bge-reranker-large 对所有召回结果重新打分,真正做到"分数高的才给模型看",而不是把一堆无关 chunk 全塞进 Prompt 里稀释答案质量。
2.3 动态参数调控:意图不同,参数不同
我们在 Agent 层实现了查询意图识别,根据问题类型动态调整检索参数:
|
问题类型 |
topK |
threshold |
BM25权重 |
rewrite |
|---|---|---|---|---|
|
默认 |
≥10 |
≤0.3 |
默认 |
on |
|
含数值/限值 |
≥10 |
降至0.3 |
默认 |
on |
|
标准内容查询(含编号) |
≥10 |
≤0.3 |
提升至0.8 |
off |
|
标准引用查询(问哪个标准) |
≥20 |
≤0.3 |
降至0.3 |
on |
💡 为什么"含标准编号"要关闭 rewrite?因为 query rewrite(同义词替换)会破坏
DB11/415-2016这类精确编号,让 BM25 无法精确命中。但追问句("确定是20%吗?")虽然也触发了数值关键词,却不含标准编号——这种情况必须保持 rewrite 开启,否则语义丢失更严重。
二、RAG 层:混合检索 + 三阶段精排
我们的检索层基于 ES(Elasticsearch) 构建,采用向量检索(KNN)+ BM25 全文检索的混合召回策略。
2.1 两路召回,各取所长
用户问题
├── 向量检索(KNN)─── 语义相似,找"说同一件事"的 chunk
└── BM25 全文检索 ─── 关键词命中,找"用了同样词"的 chunk
↓
结果合并 → Rerank 重排序(bge-reranker-large)
↓
Top-K 高质量 chunk → 注入 LLM Prompt
向量检索擅长捕捉语义,BM25 擅长精确命中专业术语(如 GB/T30685、JT/T617)。两路互补,缺一不可。
2.2 Rerank:不是所有召回都值得用
召回是粗筛,Rerank 才是精选。我们使用 bge-reranker-large 对所有召回结果重新打分,真正做到"分数高的才给模型看",而不是把一堆无关 chunk 全塞进 Prompt 里稀释答案质量。
2.3 动态参数调控:意图不同,参数不同
我们在 Agent 层实现了查询意图识别,根据问题类型动态调整检索参数:
|
问题类型 |
topK |
threshold |
BM25权重 |
rewrite |
|---|---|---|---|---|
|
默认 |
≥10 |
≤0.3 |
默认 |
on |
|
含数值/限值 |
≥10 |
降至0.3 |
默认 |
on |
|
标准内容查询(含编号) |
≥10 |
≤0.3 |
提升至0.8 |
off |
|
标准引用查询(问哪个标准) |
≥20 |
≤0.3 |
降至0.3 |
on |
💡 为什么"含标准编号"要关闭 rewrite?因为 query rewrite(同义词替换)会破坏
DB11/415-2016这类精确编号,让 BM25 无法精确命中。但追问句("确定是20%吗?")虽然也触发了数值关键词,却不含标准编号——这种情况必须保持 rewrite 开启,否则语义丢失更严重。
3.3 标准引用查询增强
问题:"运输气瓶时应遵循哪个标准?"这类问题,答案不是一段描述,而是一个标准编号(如 GB/T30685)。
挑战:包含这个编号的 chunk 内容很长(22个条款),标准号被稀释在大量文字里,向量得分偏低。
解法:检测到"问答案是哪个标准"这种模式时,追加标准编号格式特征词,同时将 topK 扩大到20,降低 BM25 权重至0.3,让向量检索主导,把正确 chunk 捞出来。
# 检测:问的是某个操作"应符合/遵循哪个标准"
_is_asking_for_standard = bool(re.search(
r'(遵循|遵守|符合|参照|依据|执行|按照|依照).{0,5}(哪个|什么|何种|哪些).{0,3}(标准|规范|规定|要求)',
question
))
if _is_asking_for_standard:
question = question + " 应严格遵守 应符合 GB/T JT/T JGJ CJJ 标准 规范"
topk = 20
term_weight_coefficient = 0.3
四、知识图谱层:LightRAG 风格的并行多路召回
向量+BM25 解决了"找到相关文本"的问题,但遇到跨文档关联问题时就力不从心了。知识图谱补上了这个短板。
4.1 图谱构建:实体+关系自动抽取
文档入库时,系统自动通过 LLM 从每个 chunk 中抽取实体(标准名称、技术指标、适用对象……)和关系(引用、包含、适用于、排除……),写入 Neo4j 图数据库,同时将实体向量化存入 Milvus。
词表缓存到 Redis,检索时快速匹配问题中出现的实体词,精准定位图谱查询起点。
4.2 并行多路召回(LightRAG mix 风格)
# 并行执行 4 路召回,最后融合
_search_neo4j() # Neo4j 图谱三元组检索
_search_entities() # 实体向量检索(Milvus)
_search_relations() # 关系向量检索(Milvus)
_search_community_reports() # 社区报告检索(文档级摘要)
- Neo4j 三元组
:找到实体的直接邻居,回答"A 和 B 是什么关系"
- 实体/关系向量
:找到语义相关的节点,补充 BM25 和向量检索的盲区
- 社区报告
:LLM 自动生成的文档级摘要,回答宏观性问题
4.3 得分融合
四路结果按 score * 权重 + relation_weight * 因子 加权融合,避免某一路噪声过大,最终和 RAG 检索结果一起注入 Prompt。
五、对话历史管理:两层分工
|
层 |
机制 |
存储 |
用途 |
|---|---|---|---|
|
Agent 层 |
前端传入 history |
内存(最近10轮) |
追问句检测、query 补全、LLM 多轮上下文 |
|
后端会话层 |
conversationId |
ES(按月分表) |
持久化历史、跨会话恢复 |
Agent 用 history 做检索增强(补全追问句语义),后端用 conversationId 做对话持续性(跨会话记忆),两层职责清晰不重叠。
六、实际效果:从"召回失败"到"精准命中"
|
问题场景 |
修复前 |
修复后 |
|---|---|---|
|
"确定是20%吗?" |
2条无关结果 |
8条相关,第一条即正确答案 |
|
"明确的不适用场景有哪些" |
0条命中 |
直接命中"本标准不适用于"条款 |
|
"运输气瓶应遵循哪个标准" |
错误章节排第一 |
含正确标准编号的条文命中 |
|
含%的追问句 |
rewrite 被误关闭 |
精准识别标准编号,动态开关 |
七、架构总结
用户提问
│
▼ [Agent 层]
├── 意图识别(追问句/范围查询/标准引用/数值查询)
├── Query 增强(补全 + 扩写 + 参数动态调整)
│
▼ [RAG 层]
├── 向量检索 KNN(Milvus/ES)
├── BM25 全文检索(ES)
└── Rerank 精排(bge-reranker-large)
│
▼ [Graph 层]
├── Neo4j 图谱三元组检索
├── 实体/关系向量检索(Milvus)
│
▼ [LLM 生成]
答案 + 来源引用
三层不是简单堆叠,而是分工明确:
-
RAG 解决"找到相关文本"
-
Graph 解决"找到关联关系"
-
Agent 解决"问题本身不够清晰"
最难的不是选择哪个技术,而是把问题拆清楚,让每一层只做它最擅长的事。
这些优化都来自真实的问答失败日志——每一条"召回0条"的日志背后,都是一次值得深挖的工程问题。
关注我们,持续分享 AI 工程化落地实践。
技术栈总览
🖥️ 编程语言
|
语言 |
用途 |
|---|---|
| Go 1.24 |
全部 Go 微服务 |
| Python 3.12+ |
RAG 服务、Agent 服务、知识图谱 |
| JavaScript / Vue |
前端 |
🏗️ 后端框架
Go 侧: Gin、gRPC、GORM、Casbin、Viper、Zap、Swag
Python 侧: Flask(Agent 服务)、FastAPI + Uvicorn(RAG 服务)、Gunicorn
🎨 前端
Vue 2 + Vuex + Vue Router + Element UI、qiankun(微前端)、ECharts、AntV G6/X6(知识图谱/工作流可视化)、Monaco Editor、fetch-event-source(SSE 流式输出)
🗄️ 数据库 / 存储
|
组件 |
用途 |
|---|---|
| MySQL |
主关系型数据库 |
| Elasticsearch 8 |
BM25 全文检索 + 历史会话存储 |
| Milvus 2.x |
向量数据库(语义检索) |
| Neo4j 5.23 |
知识图谱(GraphRAG) |
| Redis |
缓存、图谱词表、限流 |
| MinIO |
对象存储(文档文件) |
| MongoDB |
RAG 部分数据存储 |
📨 消息队列
Kafka(KRaft 模式)— 文档异步入库管道
🤖 AI / ML
LLM 接入: OpenAI SDK、DashScope/Qwen、DeepSeek、LangChain 0.3、LangGraph 0.6
向量 / 检索模型: BGE-M3(Embedding)、BGE-Reranker-Large(Rerank)、FlagEmbedding、ONNX Runtime、PyTorch + Transformers
RAG: rank-bm25、jieba 中文分词、tiktoken、pdfplumber / PyMuPDF / python-docx(文档解析)
知识图谱: NetworkX、graspologic(Leiden 社区发现)、Neo4j Python Driver
工具协议: MCP(Model Context Protocol)— Python mcp 1.12 + Go go-mcp
⚙️ 基础设施
Docker Compose、Nginx、Protobuf + gRPC、OpenTelemetry(链路追踪)、JWT + RSA、OAuth2
🏢 微服务拆分
Go 侧(8个服务)
├── bff-service BFF 网关
├── iam-service 身份认证
├── model-service 模型管理
├── knowledge-service 知识库管理
├── assistant-service 对话会话
├── rag-service RAG 调度
├── mcp-service 工具调用
└── workflow 工作流引擎
Python 侧(2个服务)
├── agent Agent 执行(Flask)
└── rag RAG + GraphRAG(FastAPI)
一句话概括:Go 微服务做业务编排,Python 做 AI 推理;ES 做文本检索,Milvus 做向量检索,Neo4j 做关系推理,三库协同构成知识底座;Kafka 打通异步数据管道;前端 qiankun 微前端保证各模块独立演进。
更多推荐



所有评论(0)