基于BERT与知识图谱的智能问答系统构建全指南
2026/9/8 12:11:08 网站建设 项目流程

1. 项目是怎么一步步撑起来的

1.1 为什么偏偏用“BERT + 知识图谱”搞问答

先说一个朋友问过我的问题:现在大语言模型这么火,随便接一个 API 就能做问答,为什么还要折腾知识图谱和 BERT?

这个问题我其实琢磨了很久。大模型确实强,但有两个毛病:一是“一本正经地胡说八道”,二是答案无法溯源。在工业落地场景里,尤其是医疗、法律、金融这种必须给出依据的垂直领域,模型胡诌一句可能就是要命的事。知识图谱的价值就在于它是结构化的、答案可解释的、能定位到具体实体和关系的,给出来的答案天然带“证据链”。

那 BERT 在这里扮演什么角色呢?它负责把用户输入的自然语言问题,映射成知识图谱里能查的东西。用户问“李白写过哪些关于月亮的诗”,人脑能秒懂,图谱可听不懂,需要先把“李白”映射到图谱里的实体节点,把“写过”映射到关系,把“关于月亮的诗”映射成具体的子图结构。BERT 做得就是这个“翻译”的活。

所以这套系统的本质是:BERT 做语义理解,知识图谱做结构化存储,两者一配合,形成一套既能理解人话、又能给出可验证答案的问答链路。

1.2 完成这套系统需要哪些基础设施

在动手写代码之前,建议先把整个技术栈捋清楚。我最终用的组合是这样的:

  • 知识图谱存储:Neo4j社区版,图数据库里最成熟的方案,自带浏览器可视化界面,社区活跃,遇到问题基本能搜到答案
  • 语义理解模型:BERT中文预训练权重,使用的是bert-base-chinese,在 Hugging Face 上直接可以下载,大概 400MB 左右
  • 编程语言:Python 3.8+,生态最齐全,深度学习和图数据库客户端都有现成的 SDK
  • Web 框架:Flask或者FastAPI,二选一,两者都可以,FastAPI 写起来更顺畅,自带 API 文档
  • 前端可视化:Vue3 + ECharts或者直接 Neo4j 自带的 Browser,前期验证阶段用 Neo4j 自带的就够了,不必在 frontend 上浪费太多时间

这里踩过一个大坑:一上来就把精力花在搞漂亮前端上,结果后端核心逻辑还没跑通,时间全浪费了。建议 Bootstrap 阶段最优先打通的是“输入问题 → BERT 解析 → 图谱查询 → 返回结果”这条链路,前端哪怕用最丑的 HTML 页面,只要链路通了,后面怎么美化都很容易补。

1.3 这套系统能解决的真实场景

做这套问答系统的初衷,是帮一个课题组做科研辅助工具。他们的需求很具体:从几百篇临床指南中抽取结构化知识,构建成图谱,然后临床医生通过自然语言提问,比如“糖尿病患者合并高血压,一线用药推荐是什么?”系统要能快速返回推荐的指南条目和证据来源。

这类需求有很明显的特点:领域固定、问题模式有限、答案必须可依循。现在用大模型做通用问答并不难,但如果要对答案负责,就得把它框在一个可控的结构里。知识图谱问答系统恰好就是这个“可控的框”。

2. 知识图谱怎么建,才不只是表面功夫

2.1 本体设计和实体关系建模

图谱的质量,七分在建模,三分在数据。很多初学者上来就抽实体、抽关系,结果建出来的图谱是又大又乱。但真正需要先想清楚的是:你的图谱需要回答什么问题,就设计什么样的本体。

比如做医疗图谱,先梳理核心实体类型(也就是知识图谱中的节点类型):

  • 疾病(Disease)
  • 症状(Symptom)
  • 药物(Drug)
  • 检查(Examination)
  • 手术(Surgery)

再定义它们之间的语义关系:

  • 疾病 —(表现为)→ 症状
  • 疾病 —(推荐用药)→ 药物
  • 疾病 —(需做检查)→ 检查
  • 疾病 —(可选治疗方式)→ 手术

这一步的核心原则是宁精勿杂。我们第一次就犯了“贪多嚼不烂”的毛病,把十几类实体、二十多种关系全部塞进图谱,后果是数据稀疏、关系冗余,查出来的答案经常是错的。后来砍到 5 类实体、8 种关系,效果反而明显提升。

2.2 实体抽取和关系抽取的工程方法

本体设计好之后,处理非结构化文本,把它转成图谱的三元组结构。

纯靠 BERT 做实体抽取,需要微调,比如用 BERT-BiLSTM-CRF 做序列标注。但在冷启动阶段,我建议先走一条更务实的路线:

第一步:基于规则和词典的候选抽取。用领域词典 + 正则表达式先把显性的实体抽出来。这个方案的召回率可能不算高,但准确率非常高,能为后续构造训练语料省下很多时间。

第二步:人工抽样校验并补充标注数据。把规则抽出来的结果放到 Label Studio 上快速人工校对,积累第一批高质量标注数据。

第三步:再用 BERT 微调实体识别和关系分类模型。当标注数据达到几千条量级之后,再上深度模型才有意义,数据太少撑不起模型的拟合能力。

用代码来表达这个过程,用hanlp或者LTP这类现成工具做初版实体抽取是最快的路径:

import hanlp # 加载中文命名实体识别模型 recognizer = hanlp.load(hanlp.pretrained.ner.MSRA_NER_BERT_BASE_ZH) text = "患者李某,男,56岁,因胸痛入院,确诊为冠心病,行冠状动脉造影检查,遵医嘱口服阿司匹林。" entities = recognizer(text) print(entities) # 输出大概是 [('胸痛', '症状', 16, 18), ('冠心病', '疾病', 27, 31), ...]

这一步只要把实体和关系按统一格式落库就行。建议统一用 JSON 行格式存储,每一行是一个三元组:

{"subject": "冠心病", "relation": "表现为", "object": "胸痛", "source": "指南文件路径或段落ID"}

source字段别忘了加,这就是答案溯源的关键。没有来源的答案,在真正做专业知识库问答时等于没有答案。

2.3 Neo4j 数据导入实操

数据清洗好之后,导入 Neo4j 有几种办法,最简单的是用py2neo一条一条地建节点和关系。数据量小的时候没问题,数据量大了就会很慢。我这边实验数据大概是 5 万个实体、20 万条关系,所以采用的是 Neo4j 官方的neo4j-admin import工具,效率高很多。

如果数据量在十万级别以下,用py2neo就够了:

from py2neo import Graph, Node, Relationship, Subgraph class Neo4jLoader: def __init__(self, uri="bolt://localhost:7687", user="neo4j", password="123456"): self.graph = Graph(uri, auth=(user, password)) def create_triple(self, subj, rel, obj, source): source_node = self.graph.nodes.match("Entity", name=subj).first() if not source_node: source_node = Node("Entity", name=subj, entity_type="unknown") self.graph.create(source_node) target_node = self.graph.nodes.match("Entity", name=obj).first() if not target_node: target_node = Node("Entity", name=obj, entity_type="unknown") self.graph.create(target_node) relation = Relationship(source_node, rel, target_node, source=source) self.graph.create(relation)

真正导入大规模数据的时候,批量操作要比逐条create快得多,用Subgraph或者UNWIND批量事务处理。导入完成后,记得在实体名称实体类型上建立索引,否则后面的查询会越来越慢。

建索引的 Cypher 语句长这样:

CREATE INDEX entity_name_index FOR (n:Entity) ON (n.name); CREATE INDEX entity_type_index FOR (n:Entity) ON (n.entity_type);

2.4 知识图谱的常见建模误区

误区一:把所有信息都塞进图里。有些属性其实用关系型字段更合适。图谱擅长表达“多跳关联”,KV 型属性存在节点字段里就行,不必把每个属性都建成关系。

误区二:忽略同义实体合并。“高血压”和“hypertension”指的是同一个病,“阿司匹林”和“乙酰水杨酸”也是同一物质。不做实体对齐,图谱查询召回率会很难看。这在知识图谱构建里叫“实体链接/实体消歧”,做专业领域问答时绕不开。

误区三:没有规范的实体类型体系。有的节点类型用中文、有的用英文,后面写查询语句的时候大小写搞死人。建议从一开始就统一节点标签用大写英文单词,比如DiseaseDrug,实体属性值存中文。

3. 打造自己专属的BERT模型,核心关键点

3.1 什么时候需要微调,什么时候直接用预训练模型

很多文章一上来就让人微调 BERT,我之前也踩过这种误区。后来发现,只做语义相似度计算或者向量化召回,用原版预训练模型往往就够了;但如果要做意图分类、实体识别、关系抽取这类任务型场景,就一定要在下游任务数据上微调。

在基于知识图谱的问答系统里,BERT 最重要的工作有两个:

第一个工作:把用户问题做意图分类和安全兜底。比如区分“唐诗相关的问题”和“临床用药相关的问题”,这本质上是文本分类。如果问题落在了系统支持的领域之外,就直接交给兜底逻辑,不要让模型硬答。

第二个工作:把用户问题和图谱中的实体/关系做语义匹配。比如用户输入“高血压吃什么药”,图谱里的标准实体名是“高血压”,标准关系是“推荐用药”,我们需要让 BERT 学会这种映射关系,不能只靠字符串精确匹配。

3.2 微调代码实操:意图分类任务全流程

因为知识图谱问答系统是垂直领域的,所以必须微调。下面以意图分类为例,给你一个可以直接复用的文本分类微调代码流程。

首先装依赖,这里我给的是 Python 环境下最稳的组合:

pip install transformers==4.37.2 torch==2.1.2

微调代码的核心流程如下:

import torch from transformers import BertTokenizer, BertForSequenceClassification, Trainer, TrainingArguments # 加载预训练模型和分词器 model_name = "bert-base-chinese" tokenizer = BertTokenizer.from_pretrained(model_name) model = BertForSequenceClassification.from_pretrained( model_name, num_labels=3 ) # 假设3个意图:诗歌问答、疾病用药、闲聊 # 训练数据格式:(问题文本, 意图标签) train_texts = ["李白的诗句有哪些", "高血压吃什么药", "今天天气怎么样"] train_labels = [0, 1, 2] # 编码数据集 def encode(texts, labels): inputs = tokenizer( texts, padding=True, truncation=True, max_length=128, return_tensors="pt" ) return { "input_ids": inputs["input_ids"], "attention_mask": inputs["attention_mask"], "labels": torch.tensor(labels) } train_dataset = encode(train_texts, train_labels) training_args = TrainingArguments( output_dir="./bert-intent-model", num_train_epochs=5, per_device_train_batch_size=8, learning_rate=2e-5, save_strategy="epoch", logging_dir="./logs" ) trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset ) trainer.train()

这里停顿一下,讲几个容易忽略的细节:

  • max_length=128不是随便定的。BERT 的输入是 512 个 token 上限,但大多数问句长度其实在 30 个字以内,设置成 128 已经可以让速度和效果达到平衡。设太长不仅显存吃不消,而且会导致注意力机制被无效 token 干扰。
  • 学习率2e-5是 BERT 微调的安全区间,太高会导致预训练知识被覆盖,太低则学不动。
  • 微调 BERT 的时候,要区分哪些层该“动得多”,哪些层该“动得少”。一般可以设置分层学习率,最后的全连接层学习率可以稍微调大,BERT 主体部分保持小学习率。这在工程上叫layer-wise learning rate decay
from transformers import AdamW # 分层学习率设置 no_decay = ["bias", "LayerNorm.weight"] optimizer_grouped_parameters = [ { "params": [p for n, p in model.named_parameters() if "classifier" in n and p.requires_grad], "lr": 5e-5 }, { "params": [p for n, p in model.named_parameters() if "classifier" not in n and p.requires_grad and not any(nd in n for nd in no_decay)], "lr": 2e-5, "weight_decay": 0.01 }, # 偏置和LayerNorm参数不衰减 ] optimizer = AdamW(optimizer_grouped_parameters)

3.3 实体链接模型:将用户问题映射为图谱可执行查询

实体链接是问答系统里最考验基本功的部分。直观理解就是:用户说“高血压吃什么药好得快”,系统需要识别出“高血压”是疾病节点,而不是症状或药物。

传统方法是做字符串模糊匹配,但中文口语变体太多,比如“血压高”和“高血压”在用户输入里经常混用。BERT 在这里可以用来做实体提及和实体名称的语义匹配

一个轻量级做法是用 BERT 把用户问题中的候选实体片段和图谱中的每个实体名字分别编码成向量,计算余弦相似度。但图谱实体往往有几十万个,逐一计算不现实。常见处理方法是“先粗筛,再精排”:

  • 粗筛阶段:用 Elasticsearch 的 BM25 检索引擎,或者直接用倒排索引,把候选实体缩小到 Top 50。
  • 精排阶段:把 Top 50 候选实体与用户问题拼接成对,输入 BERT 做二分类,判断是否匹配。

这里给出精排阶段的模型输入构造示例:

def build_entity_link_features(question, entity_candidates): features = [] for candidate in entity_candidates: # 使用[SEP]分隔问题和候选实体,让BERT学习其中的关系 text = f"[CLS] {question} [SEP] {candidate} [SEP]" features.append(text) return features # 示例 question = "血压偏高应该注意什么" candidates = ["高血压", "糖尿病", "血压计"] # 粗筛候选 inputs = build_entity_link_features(question, candidates) # 输入模型后输出每个候选是正确实体的概率

工程上,这一层直接用cross-encoder结构做精排。它比bi-encoder准确率高不少,因为问题里每个 token 都能看到候选实体的 token,信息交互更充分。缺点是速度略慢,但候选只有几十个,完全可接受。

3.4 BERT 模型在问答链路里的准确率瓶颈

如果你发现系统答非所问,九成问题不出在模型本身,而是出在训练数据不够多样候选实体排序太靠后边界情况根本没覆盖到这三个方面。

第一个问题好解决:多收集真实用户问题,扩充训练集,尤其是同义改写。第二个问题也好排查:跑几条测试数据,看粗筛环节候选实体排到第几,如果排到 50 名开外,就应该是提高粗筛召回率了。第三个问题最隐蔽:用户问的实体在训练集里完全没出现过,模型根本不知道该把它链接到哪个节点上,这种情况只能靠多积累线上日志持续训练。

4. 核心问答逻辑,把 BERT 和图谱真正串起来

4.1 整体问答流程

下面是基于 BERT 的知识图谱问答系统最核心的问答链路:

  1. 接收用户自然语言问题
  2. 意图分类模型识别问题类型(诗歌类、用药类、闲聊类)
  3. 实体链接模型识别问题中的图谱实体
  4. 基于识别结果生成候选查询模式
  5. 在 Neo4j 中执行查询并返回答案子图
  6. 答案生成模块把子图转成自然语言文本

这里面最难的是第 4 步。查询模式的生成,本质上是要把自然语言问题映射成图谱查询语言 Cypher 模板。

4.2 基于模板的查询生成策略

如果问题模式的覆盖面比较窄,完全可以用模板匹配来生成查询语句。以医疗问答为例:

模板一(单一实体查属性):

用户问:“高血压的饮食注意事项是什么?” 生成 Cypher:

MATCH (d:Disease {name: '高血压'})-[:饮食注意]->(n:Recommendation) RETURN n.content AS answer

模板二(两个实体之间查关系):

用户问:“高血压和糖尿病有什么关系?” 生成 Cypher:

MATCH p=(a:Disease {name: '高血压'})-[r]-(b:Disease {name: '糖尿病'}) RETURN type(r) AS relation, p

模板三(多跳查询):

用户问:“治疗冠心病常用的药物有哪些?” 生成 Cypher:

MATCH (d:Disease {name: '冠心病'})-[:推荐用药]->(drug:Drug) RETURN drug.name AS drug_name, drug.dosage AS dosage LIMIT 10

这里的模板化查询并非死板匹配。我们需要让 BERT 输出的意图标签去选择对应的模板,让实体链接输出替换掉模板里的{entity}占位符。如果你只有几种固定模板,那么不需要让 BERT 直接生成 Cypher 语句,这是可控性最高的方式。

4.3 用 BERT 做候选答案的重排序

当图谱查询返回多个候选答案时,还需要让 BERT 做一个重排序:哪些答案才是最贴合用户问题的。

比如问“高血压应该吃什么药”,图谱可能返回几十种药物,但我们希望把最常用、最权威的放在前面。处理办法是:用 BERT 计算“问题 + 候选答案”的匹配分,然后按分数排序。

from sentence_transformers import SentenceTransformer, util # 加载一个句向量模型(也可以用BERT的mean pooling结果) model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2") question = "高血压吃什么药" answers = ["阿司匹林", "硝苯地平", "卡托普利", "维生素C"] question_emb = model.encode(question) answer_embs = model.encode(answers) similarities = util.cos_sim(question_emb, answer_embs).squeeze(0) ranked_answers = sorted(zip(answers, similarities.tolist()), key=lambda x: x[1], reverse=True)

这里有个提醒:BGE 或者 M3E 这类中文向量模型效果会比原版 BERT 句向量好不少,强烈建议在中文场景下使用。BERT 原生向量在句对匹配上并不是最优的,通过 SentenceTransformer 封装后的模型或者专业的文本向量模型,效果会好得多。

4.4 图谱查询的代码封装

为了让前端和测试代码更清晰,我习惯把图谱查询封装成一个服务类:

from neo4j import GraphDatabase class KnowledgeGraphQA: def __init__(self, uri, user, password): self.driver = GraphDatabase.driver(uri, auth=(user, password)) def query_disease_drug(self, disease_name): """查询疾病推荐用药""" cypher = """ MATCH (d:Disease {name: $name})-[:推荐用药]->(drug:Drug) RETURN drug.name AS drug_name, drug.description AS description LIMIT 10 """ with self.driver.session() as session: result = session.run(cypher, name=disease_name) return [record.data() for record in result] def query_relation(self, entity_a, entity_b): """查询两个实体之间的关系""" cypher = """ MATCH p=(a {name: $entity_a})-[r]-(b {name: $entity_b}) RETURN type(r) AS relation_type """ with self.driver.session() as session: result = session.run(cypher, entity_a=entity_a, entity_b=entity_b) return [record.data()["relation_type"] for record in result] def close(self): self.driver.close()

4.5 混合检索:向量召回与图谱查询相结合

在真实落地场景里,我更推荐“混合检索”的思路:

  • 第一步,用向量检索召回与用户问题最相关的图谱子图或文档片段
  • 第二步,用图谱查询获取精确的结构化答案
  • 第三步,把两者结果合并,做一个重排序

为什么要这么做?因为知识图谱总有覆盖不到的长尾问题,但文档库里可能就有答案片段。混合检索可以让系统在“精确知识”与“开放检索”之间取得平衡。这在业界也是主流做法,和 RAG(检索增强生成)的思路完全一致。

5. 从本地脚本到完整系统的整合路径

5.1 用 FastAPI 把模型和服务串成接口

模型调通之后,把它封装成 Web 服务接口,方便对接前端或小程序。我这里用的是 FastAPI,代码结构大致如下:

from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class Question(BaseModel): text: str class Answer(BaseModel): answer: str evidence: list # 加载训练好的模型 model = load_bert_model() qa = KnowledgeGraphQA("bolt://localhost:7687", "neo4j", "123456") @app.post("/ask", response_model=Answer) def ask(question: Question): intent = model.predict_intent(question.text) entities = model.predict_entities(question.text) if intent == "disease_drug": answers = qa.query_disease_drug(entities.get("disease")) return Answer(answer=answers[0]["drug_name"] if answers else "未找到相关答案", evidence=answers) return Answer(answer="暂不支持该类型问题", evidence=[])

5.2 前端如何展示知识图谱

用 Vue3 加 ECharts 把图谱查询结果可视化,是增强交互感的加分项。ECharts 的graph类型可以直接渲染图结构数据。

<template> <div ref="chart" style="width: 100%; height: 600px;"></div> </template> <script setup> import * as echarts from 'echarts'; import { onMounted, ref } from 'vue'; const chart = ref(null); onMounted(() => { const myChart = echarts.init(chart.value); const option = { tooltip: {}, series: [{ type: 'graph', layout: 'force', roam: true, data: [ { name: '高血压', symbolSize: 50, category: 0 }, { name: '冠心病', symbolSize: 40, category: 0 }, { name: '硝苯地平', symbolSize: 30, category: 1 }, ], links: [ { source: '高血压', target: '冠心病' }, { source: '高血压', target: '硝苯地平' }, ], categories: [{ name: '疾病' }, { name: '药物' }], force: { repulsion: 200 } }] }; myChart.setOption(option); }); </script>

前端只是系统的一小部分,但它直观地帮你验证了图谱问答的效果。有了可视化以后,给导师、给团队展示成果的说服力会大很多。

5.3 部署时的资源规划和模型加载优化

BERT 模型体积不小,在 CPU 服务器上推理速度偏慢,这里提供几个常用的加速手段:

方案一:ONNX Runtime 加速。把 PyTorch 模型转成 ONNX 格式,CPU 推理速度能提升 2 到 4 倍:

from transformers import BertForSequenceClassification from transformers.onnx import export from pathlib import Path # 导出ONNX model = BertForSequenceClassification.from_pretrained("./bert-intent-model") onnx_output = Path("bert-intent-model.onnx") export( model=model, config=model.config, opset=12, output=onnx_output )

方案二:模型蒸馏压缩。在大模型训练好后,用蒸馏方式把 BERT-large 缩小为 TinyBERT 或 DistilBERT 版本,推理速度能快 5 至 10 倍。

方案三:batch 推理。接口层做请求缓存和合并,把多个请求攒成 batch 再一次性过模型。系统并发不高的情况下这是最简单的优化。

5.4 完整系统的部署架构

部署层面,不建议在一台机器上把所有东西都跑起来,资源不足会导致全链路雪崩。架构上建议这样拆分:

  • 应用服务层:FastAPI 提供问答接口,承载 BERT 推理
  • 图数据库层:Neo4j 独立部署,或将图谱数据放在云上
  • 缓存层:Redis 缓存高频问题和热门答案,大幅降低模型调用次数
  • 日志层:记录每次问答的中间结果,这些数据是模型迭代优化的燃料

6. 实操中踩过的坑和解决思路

6.1 环境搭建阶段:依赖冲突和 Python 版本问题

我在环境搭建阶段浪费了大约四个多小时,原因是多个项目共用一个 Python 环境。TensorFlow、PyTorch、transformers 版本互相打架。最后重新用 conda 给这个项目单独建了虚拟环境才彻底解决:

conda create -n kgqa python=3.9 conda activate kgqa pip install torch==2.1.2 transformers==4.37.2 fastapi uvicorn py2neo neo4j pandas

原则很简单:项目环境必须隔离,倚赖锁版本,不要轻信 pip install 全不指定版本。

6.2 中文编码问题:JSON 存储与 Neo4j 中文乱码

中文知识图谱数据在 JSON 和 Neo4j 之间转移时,乱码是老问题。根本原因多半是 JSON 入库时没有声明 UTF-8 编码。统一的处理方式是读取和写入时都显式指定编码:

import json with open("triples.json", "r", encoding="utf-8") as f: triples = json.load(f)

6.3 实体识别不准导致的连锁反应

最典型的现象:用户问“高血压怎么治疗”,系统把“高血压”识别成“高血脂”,图谱查询直接返回空结果。这时候别急着调模型,先看实体链接是哪个环节出了问题。大概率是候选实体粗筛阶段就把“高血脂”排在了前面。

处理办法是给粗筛加一个领域词典加权,把高频常见实体优先排在前面。具体实现就是在 BM25 打分基础上加一个词典命中加分项。

6.4 Neo4j 查询超时

当图谱数据量达到百万级关系以上,复杂的多跳查询很容易超时。有两个关键优化点:

第一点是加索引,这在前文已经提过,理论上能解决 80% 的性能问题。第二点是为高频查询做 Redis 缓存。

import redis cache = redis.Redis(host="localhost", port=6379, db=0) def get_answer_with_cache(question): cached = cache.get(question) if cached: return json.loads(cached) # 执行图谱查询 answer = execute_graph_query(question) cache.set(question, json.dumps(answer, ensure_ascii=False), ex=3600) return answer

6.5 模型在推理阶段的内存占用

BERT-base 模型内存占用大约在 400MB 到 500MB 之间,多个模型叠加部署容易撑爆内存。建议:

  • torch.no_grad()包裹推理过程,避免显存持续增加
  • 定期清理缓存,必要时用gc.collect()
  • 同一个区域的问题用同一个模型,不要一个请求加载一次模型

6.6 效果不好时的排查顺序

如果做出来的问答系统效果不理想,按照这个顺序排查,效率最高:

  1. 先看意图分类是不是分错了
  2. 再看实体链接是不是链接错了
  3. 然后看图谱查询模板是不是匹配正确
  4. 最后看答案重排序把有用的答案排到后面没有

绝大多数问题都出在前两层,而不是最后的模型效果。

7. 从“能跑”到“好用”的几个可扩展方向

7.1 接入大模型做答案润色

当前系统返回的是一段结构化答案,可读性不佳。后续可以在图谱查询结果的基础上,把结果当作上下文,让大模型生成一段通顺的自然语言回答。这样做的好处是:大模型负责表达,图谱负责事实,各司其职。

这种方式比直接裸用大模型安全得多,因为所有的事实内容都来自图谱,模型的自由发挥空间被压缩到一个可控的范围内。

7.2 加入用户反馈闭环

把用户对答案的反馈(点赞还是点踩)记录下来,定期分析。反馈数据是 BERT 模型和实体链接模块迭代训练的最好语料。

7.3 支持多轮对话

当前系统是单轮问答,多轮对话场景下容易丢失上文。可以用一个上下文管理器记录用户前面提到的实体和意图。比如用户先问“高血压怎么办”,再问“它会引发什么并发症”,系统要知道第二问里的“它”指代的是“高血压”。

当时搭这套系统的初衷很简单,就是想做一套“不胡说八道的智能问答”。做完以后回头看,最值钱的部分不是 BERT 模型本身,而是把语义理解、图数据库、查询工程串起来时的那些取舍和排坑经验。真正做工程,难点从来不在单点技术上,而在于怎么让这些技术在一个现实约束下跑得稳、跑得快、跑得准。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询