AI应用的领域知识注入:让大模型成为你领域的专家
AI应用的领域知识注入:让大模型成为你领域的专家
date: 2026-10-08 tags: [领域知识, Fine-tuning, RAG, 提示词工程, Spring AI, Java]
一、真实故事:从65%到92%的跨越
2025年3月,李明站在会议室里,面对一屋子医院信息科的同事,汗流浃背。
他是某三甲医院信息化项目的Java技术负责人,团队花了4个月时间,接入了某国产大模型,上线了"智慧医疗问答助手"。发布那天,院长亲自来体验。第一个问题就来了:
"患者服用华法林期间,INR值3.8,是否需要调整剂量?"
系统给出了回答:"建议您咨询专业医生……"
院长沉默了3秒,问:"这不就是个搜索引擎吗?"
李明当场无地自容。
事后复盘,他们测了500道专科医疗题,准确率只有65%。更严重的是,对于药物相互作用、禁忌症这类关键问题,模型经常给出过于保守或完全错误的回答。
问题出在哪里?
通用大模型的训练数据虽然包含了大量医疗文献,但医院的临床诊疗规范、药物配伍禁忌表、科室专属流程这些"私有领域知识",模型根本不知道。医院的《华法林抗凝治疗管理规范》明确规定:INR在3.0-3.5暂不调整,INR>3.5需减量10-20%,INR>4.5需停药。这些规则存在Word文档里,从来没喂给模型。
接下来两个月,李明的团队系统地做了领域知识注入改造:
- 提示词注入:将核心临床规则结构化为系统提示
- RAG注入:把2000+页诊疗规范向量化,检索增强
- Fine-tuning:用1万条专家标注的问答对微调模型
最终测试结果:专科问题准确率从65%提升到92%,药物禁忌问题准确率从48%提升到89%。
这就是本文要系统讲解的核心命题:如何让大模型真正掌握你的领域知识?
二、领域知识注入的本质:弥合通用与专业的鸿沟
2.1 为什么通用模型不够用
大模型在训练阶段看过海量文本,但有几个根本性的局限:
知识截止日期:模型训练数据有截止日期,你的最新业务规则它不知道。
私有知识盲区:企业内部文档、行业特定规范、公司自己的产品手册,这些从来不在训练集里。
领域深度不足:医学、法律、金融这些领域,有大量需要精确性的专业知识。通用模型倾向于给出"安全"但模糊的答案。
术语歧义:同一个词在不同领域含义完全不同。"对冲"在金融是套期保值,在军事是战术概念。
2.2 三条注入路径的本质差异
| 维度 | 提示词注入 | RAG注入 | Fine-tuning |
|---|---|---|---|
| 知识容量 | 小(~32K tokens) | 大(理论无限) | 大(固化到权重) |
| 更新成本 | 极低(改文本) | 低(重新向量化) | 高(重新训练) |
| 推理延迟 | 低 | 中(检索耗时) | 低 |
| 专业深度 | 中 | 中高 | 高 |
| 适用场景 | 规则少、规则固定 | 文档多、更新频繁 | 需要风格/深度 |
| 启动成本 | 小时级 | 天级 | 周级 |
选择策略:先做提示词注入验证效果,不够再加RAG,还不够再考虑微调。大多数B端场景,RAG就够了。
三、路径1:系统提示词注入
3.1 结构化领域规则的设计原则
提示词注入不是把文档全部粘贴进去。而是提炼规则,并按照机器可理解的结构组织。
原则一:规则要可判断
模糊的规则:
// 坏的例子
"""注意用药安全"""可判断的规则:
// 好的例子
"""
华法林剂量调整规则(INR监测):
- INR < 2.0: 增加剂量10%,3天后复查
- INR 2.0-3.0: 治疗范围内,继续原剂量
- INR 3.1-3.5: 维持原剂量,1周后复查
- INR 3.6-4.0: 减少剂量10%,暂停一次,3天后复查
- INR > 4.0: 停药,评估出血风险,每天监测INR
"""原则二:层次化组织
// 系统提示词构建器
@Component
public class MedicalPromptBuilder {
public String buildSystemPrompt(String specialty) {
return """
你是一名资深%s专科医疗助手,具备以下能力和约束:
## 你的知识边界
1. 遵循中华医学会%s学分会最新指南(2024版)
2. 参考本院《%s科诊疗规范》第3版
3. 药物信息以国家药品监督管理局批准说明书为准
## 回答规则
- 必须引用具体指南条款或规范章节
- 涉及用药剂量必须区分成人/儿童/特殊人群
- 出现以下关键词必须提示就医:胸痛、意识障碍、大量出血
## 禁止事项
- 不得给出确定性诊断
- 不得推荐未经NMPA批准的治疗方法
- 不得引用2020年之前的过时指南
## 回答格式
```
【评估】: [对问题的专业判断,1-2句]
【依据】: [引用的具体规则/指南,带章节编号]
【建议】: [具体可操作建议,分步骤]
【注意】: [需要特别关注的风险点]
```
""".formatted(specialty, specialty, specialty);
}
}3.2 医疗/法律/金融三个领域的提示词模板
医疗领域:
@Service
public class MedicalSystemPrompt {
private static final String DRUG_INTERACTION_RULES = """
## 高风险药物相互作用规则(必须检查)
### 华法林相互作用
- 增强抗凝:阿司匹林、NSAIDs、抗生素(甲硝唑、氟康唑)
-> 处理:INR监测频率加倍,必要时减量15-25%
- 减弱抗凝:维生素K、利福平、卡马西平
-> 处理:INR监测频率加倍,必要时增量10-20%
### 地高辛相互作用
- 增加毒性:胺碘酮、奎尼丁、维拉帕米(地高辛浓度升高50-100%)
-> 处理:地高辛减量50%,监测血药浓度
### MAOI禁忌
- 禁止与以下合用(14天清洗期):
SSRIs、SNRIs、哌替啶、曲马多
-> 风险:5-羟色胺综合征,危及生命
""";
public String buildDrugConsultationPrompt() {
return """
你是医院药学部资深临床药师,专注于药物咨询。
%s
## 回答要求
当用户咨询药物问题时:
1. 首先检查是否涉及上述高风险相互作用
2. 明确区分"禁忌"(绝对不可以)和"慎用"(需监测)
3. 所有剂量建议必须注明依据(说明书/指南/文献)
4. 如涉及高风险情况,必须建议立即联系临床医生
""".formatted(DRUG_INTERACTION_RULES);
}
}法律领域:
@Service
public class LegalSystemPrompt {
public String buildContractReviewPrompt(String jurisdiction) {
return """
你是专注于%s地区商业合同的法律助手。
## 适用法律体系
- 主要适用:《合同法》《民法典》合同编
- 特殊交易:参照《公司法》《劳动合同法》《电子商务法》
## 必须标记的高风险条款
1. 违约金超过损失的30%(可能被认定过高而调整)
2. 排除一方根本违约的免责条款(无效)
3. 格式合同中加重对方责任的条款(需特别提示)
4. 仲裁条款中管辖权不明确(可能导致无法仲裁)
## 回答格式
- 风险等级:高/中/低
- 具体条款定位(第X条第X款)
- 风险说明(法律依据)
- 修改建议(具体措辞)
## 明确边界
本回答仅供参考,不构成正式法律意见,重大决策请咨询执业律师。
""".formatted(jurisdiction);
}
}金融领域:
@Service
public class FinanceSystemPrompt {
public String buildInvestmentAdvisoryPrompt(String riskLevel) {
return """
你是持牌投资顾问助手,服务%s风险偏好客户。
## 风险适配规则
%s
## 必须遵守的监管规则
- 不得承诺收益(违反《证券法》第88条)
- 不得诱导客户超越风险承受能力投资
- 涉及具体证券必须声明"不构成投资建议"
- 过往业绩不代表未来表现(每次提及历史数据时声明)
## 回答结构
1. 理解客户需求
2. 匹配适合的产品类型(而非具体产品)
3. 风险提示(必须具体而非套话)
4. 建议下一步(咨询理财经理/阅读产品说明书)
""".formatted(riskLevel, getRiskRules(riskLevel));
}
private String getRiskRules(String riskLevel) {
return switch (riskLevel) {
case "保守" -> "只推荐R1-R2级产品(货币基金、国债、定存)";
case "稳健" -> "可推荐R1-R3级产品,权益类不超过20%";
case "均衡" -> "可推荐R1-R4级产品,权益类不超过50%";
case "进取" -> "可推荐R1-R5级产品,需额外风险提示";
default -> "未知风险等级,需先完成风险测评";
};
}
}3.3 提示词注入的上下文窗口管理
当规则较多时,需要动态管理注入内容:
@Service
public class AdaptivePromptManager {
private static final int MAX_SYSTEM_TOKENS = 4096;
private final TokenCounter tokenCounter;
// 按优先级注入规则,超出Token限制时截断低优先级内容
public String buildAdaptivePrompt(List<RuleBlock> rules, String userQuery) {
// 首先分析查询,确定最相关的规则类别
List<String> relevantCategories = analyzeQueryCategories(userQuery);
StringBuilder prompt = new StringBuilder();
prompt.append(getBaseSystemPrompt());
int usedTokens = tokenCounter.count(prompt.toString());
// 按优先级和相关性排序规则
List<RuleBlock> sortedRules = rules.stream()
.sorted(Comparator
.comparingInt(r -> relevantCategories.contains(r.getCategory()) ? 0 : 1))
.toList();
for (RuleBlock rule : sortedRules) {
String ruleText = rule.toText();
int ruleTokens = tokenCounter.count(ruleText);
if (usedTokens + ruleTokens > MAX_SYSTEM_TOKENS) {
// 超出限制,记录未注入的规则(可通过RAG补充)
log.warn("规则 {} 因Token限制未注入,建议使用RAG", rule.getId());
break;
}
prompt.append("\n").append(ruleText);
usedTokens += ruleTokens;
}
return prompt.toString();
}
private List<String> analyzeQueryCategories(String query) {
// 使用轻量级分类模型或关键词匹配确定查询类别
List<String> categories = new ArrayList<>();
if (query.contains("药") || query.contains("用量") || query.contains("禁忌")) {
categories.add("medication");
}
if (query.contains("手术") || query.contains("操作") || query.contains("禁食")) {
categories.add("procedure");
}
if (query.contains("检查") || query.contains("化验") || query.contains("影像")) {
categories.add("examination");
}
return categories;
}
}四、路径2:RAG注入(完整Spring AI实现)
4.1 RAG的核心架构
4.2 领域文档的向量化管道
// 文档处理配置
@Configuration
public class DocumentPipelineConfig {
@Bean
public DocumentReader pdfReader() {
return new PagePdfDocumentReader(
"classpath:medical-guidelines/*.pdf",
PdfDocumentReaderConfig.builder()
.withPageTopMargin(0)
.withPageExtractedTextFormatter(
ExtractedTextFormatter.builder()
.withNumberOfTopPagesToSkipBeforeDelete(0)
.withDeleteRedundantWhitespace(true)
.build())
.withPagesPerDocument(1)
.build()
);
}
@Bean
public TextSplitter medicalTextSplitter() {
// 医疗文档需要保持章节完整性
return TokenTextSplitter.builder()
.withChunkSize(800) // 每块800 tokens
.withMinChunkSizeChars(350)
.withMinChunkLengthToEmbed(5)
.withMaxNumChunks(10000)
.withKeepSeparator(true)
.build();
}
@Bean
public EmbeddingModel embeddingModel() {
// 使用专业医疗embedding模型(效果优于通用模型20-30%)
return new OpenAiEmbeddingModel(
OpenAiEmbeddingOptions.builder()
.withModel("text-embedding-3-large")
.withDimensions(1536)
.build()
);
}
}// 文档摄入服务
@Service
@Slf4j
public class DomainDocumentIngestionService {
private final VectorStore vectorStore;
private final TextSplitter textSplitter;
private final DocumentMetadataEnricher metadataEnricher;
public IngestionResult ingestDocument(DomainDocument domainDoc) {
log.info("开始摄入文档: {}", domainDoc.getTitle());
// 1. 加载文档
List<Document> rawDocs = loadDocument(domainDoc);
log.info("加载原始文档 {} 页", rawDocs.size());
// 2. 预处理(清洗、格式化)
List<Document> cleanedDocs = rawDocs.stream()
.map(this::preprocessDocument)
.filter(doc -> doc.getContent().length() > 50) // 过滤空页
.toList();
// 3. 分块
List<Document> chunks = textSplitter.apply(cleanedDocs);
log.info("文档分块结果: {} 块", chunks.size());
// 4. 元数据增强
List<Document> enrichedChunks = chunks.stream()
.map(chunk -> metadataEnricher.enrich(chunk, domainDoc))
.toList();
// 5. 批量向量化并存储
int batchSize = 100;
int totalBatches = (enrichedChunks.size() + batchSize - 1) / batchSize;
for (int i = 0; i < enrichedChunks.size(); i += batchSize) {
List<Document> batch = enrichedChunks.subList(
i, Math.min(i + batchSize, enrichedChunks.size())
);
vectorStore.add(batch);
log.info("已处理 {}/{} 批次", (i / batchSize + 1), totalBatches);
}
return IngestionResult.builder()
.documentId(domainDoc.getId())
.totalChunks(enrichedChunks.size())
.status(IngestionStatus.SUCCESS)
.build();
}
private Document preprocessDocument(Document doc) {
String content = doc.getContent();
// 医疗文档特殊清洗
content = content.replaceAll("\\f", "\n") // 换页符
.replaceAll("\\s{3,}", "\n") // 多余空白
.replaceAll("(?m)^\\d+$", "") // 页码
.trim();
return new Document(content, doc.getMetadata());
}
}// 元数据增强器 - 关键!影响检索精度
@Component
public class DocumentMetadataEnricher {
public Document enrich(Document chunk, DomainDocument domainDoc) {
Map<String, Object> metadata = new HashMap<>(chunk.getMetadata());
// 文档来源信息
metadata.put("source", domainDoc.getTitle());
metadata.put("document_type", domainDoc.getType().name()); // GUIDELINE/PROTOCOL/MANUAL
metadata.put("specialty", domainDoc.getSpecialty());
metadata.put("version", domainDoc.getVersion());
metadata.put("effective_date", domainDoc.getEffectiveDate().toString());
// 内容分类(用于检索过滤)
metadata.put("content_category", classifyContent(chunk.getContent()));
// 权威度评分(影响重排序)
metadata.put("authority_score", calculateAuthorityScore(domainDoc));
// 时效性(较新的文档得分更高)
long daysSincePublish = ChronoUnit.DAYS.between(
domainDoc.getPublishDate(), LocalDate.now()
);
metadata.put("freshness_score", Math.max(0.0, 1.0 - daysSincePublish / 730.0));
return new Document(chunk.getId(), chunk.getContent(), metadata);
}
private String classifyContent(String content) {
// 简单关键词分类
if (content.contains("剂量") || content.contains("用法")) return "DOSAGE";
if (content.contains("禁忌") || content.contains("不良反应")) return "SAFETY";
if (content.contains("诊断") || content.contains("鉴别")) return "DIAGNOSIS";
if (content.contains("手术") || content.contains("操作步骤")) return "PROCEDURE";
return "GENERAL";
}
private double calculateAuthorityScore(DomainDocument doc) {
return switch (doc.getType()) {
case NATIONAL_GUIDELINE -> 1.0;
case SOCIETY_GUIDELINE -> 0.9;
case HOSPITAL_PROTOCOL -> 0.8;
case TEXTBOOK -> 0.7;
case LITERATURE -> 0.6;
default -> 0.5;
};
}
}4.3 检索增强的查询处理
@Service
public class EnhancedRAGService {
private final VectorStore vectorStore;
private final ChatClient chatClient;
private final TermExpander termExpander;
private final RerankerService rerankerService;
public RAGResponse query(String userQuestion, String specialty) {
// 步骤1:查询改写(扩展同义词和专业术语)
String expandedQuery = termExpander.expand(userQuestion, specialty);
log.debug("原始查询: {}, 扩展后: {}", userQuestion, expandedQuery);
// 步骤2:混合检索(向量+关键词)
List<Document> vectorResults = vectorStore.similaritySearch(
SearchRequest.builder()
.query(expandedQuery)
.topK(20)
.similarityThreshold(0.65)
.filterExpression("specialty == '" + specialty + "'")
.build()
);
// 步骤3:重排序(Cross-Encoder精排)
List<Document> rerankedDocs = rerankerService.rerank(
userQuestion, vectorResults, 5
);
// 步骤4:构建上下文
String context = buildContext(rerankedDocs);
// 步骤5:生成答案
String answer = chatClient.prompt()
.system(buildRAGSystemPrompt(specialty))
.user(u -> u.text(RAG_TEMPLATE)
.param("context", context)
.param("question", userQuestion))
.call()
.content();
// 步骤6:提取引用来源
List<Citation> citations = extractCitations(rerankedDocs);
return RAGResponse.builder()
.answer(answer)
.citations(citations)
.retrievedDocCount(vectorResults.size())
.build();
}
private static final String RAG_TEMPLATE = """
请基于以下领域文档回答问题。
## 参考文档
{context}
## 用户问题
{question}
## 要求
1. 只使用参考文档中的信息作答
2. 明确标注信息来源(如"根据《华法林治疗管理规范》第3.2节")
3. 如果参考文档中没有相关信息,明确说明"参考文档中未找到相关内容"
4. 不要基于通用知识补充文档中没有的内容
""";
private String buildContext(List<Document> docs) {
StringBuilder context = new StringBuilder();
for (int i = 0; i < docs.size(); i++) {
Document doc = docs.get(i);
context.append(String.format("""
### 文档%d
来源:%s(%s版本)
类型:%s
内容:
%s
""",
i + 1,
doc.getMetadata().get("source"),
doc.getMetadata().get("version"),
doc.getMetadata().get("document_type"),
doc.getContent()
));
}
return context.toString();
}
}4.4 重排序服务实现
@Service
public class RerankerService {
private final RestTemplate restTemplate;
@Value("${reranker.api.url}")
private String rerankerApiUrl;
// 使用Cross-Encoder模型对检索结果重排序
public List<Document> rerank(String query, List<Document> candidates, int topK) {
if (candidates.isEmpty()) return candidates;
// 构建重排序请求
List<RerankItem> items = candidates.stream()
.map(doc -> new RerankItem(doc.getId(), doc.getContent()))
.toList();
RerankRequest request = new RerankRequest(query, items, topK);
RerankResponse response = restTemplate.postForObject(
rerankerApiUrl, request, RerankResponse.class
);
if (response == null || response.getResults().isEmpty()) {
log.warn("重排序服务无响应,使用原始顺序");
return candidates.subList(0, Math.min(topK, candidates.size()));
}
// 按重排序得分排列
Map<String, Document> docMap = candidates.stream()
.collect(Collectors.toMap(Document::getId, d -> d));
return response.getResults().stream()
.sorted(Comparator.comparingDouble(RerankResult::getScore).reversed())
.map(result -> docMap.get(result.getDocId()))
.filter(Objects::nonNull)
.limit(topK)
.toList();
}
}五、路径3:Fine-tuning的成本/收益分析框架
5.1 什么情况下值得微调
不要轻易微调,先算清楚账:
微调成本估算(GPT-4o级别模型):
| 数据规模 | 训练Token | 估算成本 | 训练时间 |
|---|---|---|---|
| 1000条QA | ~200万 | ~$60 | 2-4小时 |
| 5000条QA | ~1000万 | ~$300 | 8-12小时 |
| 1万条QA | ~2000万 | ~$600 | 1-2天 |
| 5万条QA | ~1亿 | ~$3000 | 3-5天 |
收益评估框架:
public class FineTuningROICalculator {
public FineTuningROI calculate(FineTuningScenario scenario) {
// 成本计算
double trainingCost = scenario.getTrainingTokens() * 0.000030; // $0.03/1K tokens
double dataAnnotationCost = scenario.getDataSamples() * scenario.getCostPerSample();
double infrastructureCost = scenario.getTrainingHours() * 2.5; // GPU成本
double totalCost = trainingCost + dataAnnotationCost + infrastructureCost;
// 收益计算
double accuracyImprovement = scenario.getExpectedAccuracyGain(); // 0.0-1.0
double dailyQueries = scenario.getDailyQueryVolume();
double errorCostPerQuery = scenario.getCostPerWrongAnswer(); // 业务成本
double dailySaving = dailyQueries * accuracyImprovement * errorCostPerQuery;
double paybackDays = totalCost / dailySaving;
double yearlyROI = (dailySaving * 365 - totalCost) / totalCost * 100;
return FineTuningROI.builder()
.totalCost(totalCost)
.dailySaving(dailySaving)
.paybackPeriodDays((int) paybackDays)
.yearlyROIPercent(yearlyROI)
.recommendation(yearlyROI > 200 ? "强烈推荐" :
yearlyROI > 100 ? "建议考虑" : "谨慎决策")
.build();
}
}5.2 微调数据集的准备规范
// 微调数据格式标准化
public class FineTuningDatasetBuilder {
// OpenAI微调数据格式
public String buildOpenAIFormat(List<QAPair> qaPairs, String systemPrompt) {
List<Map<String, Object>> dataset = qaPairs.stream()
.map(qa -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("messages", List.of(
Map.of("role", "system", "content", systemPrompt),
Map.of("role", "user", "content", qa.getQuestion()),
Map.of("role", "assistant", "content", qa.getAnswer())
));
return item;
})
.toList();
// 每行一个JSON(JSONL格式)
return dataset.stream()
.map(item -> {
try {
return objectMapper.writeValueAsString(item);
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.collect(Collectors.joining("\n"));
}
// 数据质量检查
public DataQualityReport validateDataset(List<QAPair> dataset) {
int totalCount = dataset.size();
// 检查1:答案长度分布
DoubleSummaryStatistics lengthStats = dataset.stream()
.mapToDouble(qa -> qa.getAnswer().length())
.summaryStatistics();
// 检查2:重复问题检测
long duplicateCount = dataset.stream()
.collect(Collectors.groupingBy(QAPair::getQuestion, Collectors.counting()))
.values().stream()
.filter(count -> count > 1)
.count();
// 检查3:空答案
long emptyAnswers = dataset.stream()
.filter(qa -> qa.getAnswer().isBlank())
.count();
return DataQualityReport.builder()
.totalSamples(totalCount)
.avgAnswerLength((int) lengthStats.getAverage())
.duplicateQuestions((int) duplicateCount)
.emptyAnswers((int) emptyAnswers)
.qualityScore(calculateQualityScore(totalCount, duplicateCount, emptyAnswers))
.ready(duplicateCount < totalCount * 0.05 && emptyAnswers == 0)
.build();
}
}六、知识图谱增强RAG
6.1 为什么需要知识图谱
纯向量检索的问题:缺乏结构化关联。
例如:用户问"阿司匹林禁忌",向量检索会找到提到"阿司匹林禁忌"的文档段落。但如果知识库里记录了"阿司匹林属于NSAIDs","NSAIDs的通用禁忌包括XXX",向量检索无法自动推导出这个关联。
知识图谱能显式表达这种层级和关联关系。
6.2 领域知识图谱的Java实现
// 知识图谱节点
@Entity
@Table(name = "kg_nodes")
public class KnowledgeNode {
@Id
private String id;
private String label; // 节点类型:Drug/Disease/Symptom/Procedure
private String name; // 实体名称
private String aliases; // 别名(JSON数组)
private String properties; // 属性(JSON)
@OneToMany(mappedBy = "fromNode", cascade = CascadeType.ALL)
private List<KnowledgeEdge> outEdges;
}
// 知识图谱边
@Entity
@Table(name = "kg_edges")
public class KnowledgeEdge {
@Id
private String id;
@ManyToOne
@JoinColumn(name = "from_node_id")
private KnowledgeNode fromNode;
@ManyToOne
@JoinColumn(name = "to_node_id")
private KnowledgeNode toNode;
private String relation; // CONTRAINDICATED_WITH/TREATS/CAUSES/IS_A
private Double confidence; // 置信度
private String evidence; // 来源依据
}// 图谱增强的RAG查询
@Service
public class GraphEnhancedRAGService {
private final KnowledgeGraphRepository graphRepo;
private final VectorStore vectorStore;
private final ChatClient chatClient;
public String query(String question) {
// 步骤1:从问题中提取实体
List<String> entities = extractEntities(question);
log.debug("提取实体: {}", entities);
// 步骤2:图谱扩展(找到相关的关联实体)
Set<String> expandedTerms = new HashSet<>(entities);
for (String entity : entities) {
// 查找同义词和上位词
List<KnowledgeNode> relatedNodes = graphRepo.findRelatedNodes(
entity, List.of("IS_A", "SYNONYM_OF", "BELONGS_TO"), 2
);
relatedNodes.stream()
.map(KnowledgeNode::getName)
.forEach(expandedTerms::add);
}
// 步骤3:用扩展后的实体做向量检索
String expandedQuery = String.join(" ", expandedTerms) + " " + question;
List<Document> docs = vectorStore.similaritySearch(
SearchRequest.query(expandedQuery).withTopK(10)
);
// 步骤4:提取图谱关系作为结构化上下文
String graphContext = buildGraphContext(entities);
// 步骤5:组合文档上下文和图谱上下文
String fullContext = graphContext + "\n\n## 相关文档\n" + buildDocContext(docs);
return chatClient.prompt()
.user(u -> u.text("""
基于以下知识(包括结构化知识图谱和文档),回答问题:
{context}
问题:{question}
""")
.param("context", fullContext)
.param("question", question))
.call()
.content();
}
private String buildGraphContext(List<String> entities) {
StringBuilder context = new StringBuilder("## 知识图谱关系\n");
for (String entity : entities) {
List<KnowledgeEdge> edges = graphRepo.findEdgesByEntityName(entity);
for (KnowledgeEdge edge : edges) {
context.append(String.format("- %s [%s] %s(置信度: %.0f%%)\n",
edge.getFromNode().getName(),
edge.getRelation(),
edge.getToNode().getName(),
edge.getConfidence() * 100
));
}
}
return context.toString();
}
private List<String> extractEntities(String question) {
// 使用NER模型或规则提取实体
// 这里用简化的基于词典的方式示例
return entityDictionary.findEntities(question);
}
}七、专业术语处理:领域词典对查询的影响
7.1 术语扩展器实现
@Service
public class DomainTermExpander {
private final Map<String, List<String>> termSynonyms;
private final Map<String, String> abbreviationMap;
@PostConstruct
public void loadDictionaries() {
// 医疗术语词典示例
termSynonyms = new HashMap<>();
termSynonyms.put("心肌梗死", List.of("心梗", "MI", "AMI", "急性心肌梗死", "心脏病发作"));
termSynonyms.put("高血压", List.of("血压高", "HTN", "hypertension", "原发性高血压"));
termSynonyms.put("糖尿病", List.of("血糖高", "DM", "diabetes mellitus", "消渴症"));
abbreviationMap = new HashMap<>();
abbreviationMap.put("INR", "国际标准化比值 凝血功能");
abbreviationMap.put("BNP", "B型钠尿肽 心衰标志物");
abbreviationMap.put("HbA1c", "糖化血红蛋白 血糖控制");
abbreviationMap.put("eGFR", "估算肾小球滤过率 肾功能");
}
public String expand(String query, String domain) {
String expanded = query;
// 1. 展开缩写
for (Map.Entry<String, String> entry : abbreviationMap.entrySet()) {
if (query.contains(entry.getKey())) {
expanded += " " + entry.getValue();
}
}
// 2. 添加同义词
for (Map.Entry<String, List<String>> entry : termSynonyms.entrySet()) {
boolean foundSynonym = entry.getValue().stream()
.anyMatch(synonym -> query.contains(synonym));
if (query.contains(entry.getKey()) || foundSynonym) {
// 将所有同义词加入查询
String synonyms = String.join(" ", entry.getValue());
expanded += " " + entry.getKey() + " " + synonyms;
}
}
return expanded.trim();
}
// 查询改写:将口语化问题改为专业表述
public String rewrite(String colloquialQuery, ChatClient chatClient) {
return chatClient.prompt()
.system("""
你是医疗术语专家。将以下口语化的医疗问题改写为专业医学表述。
只改写术语,保持问题原意。直接输出改写后的问题,不要解释。
""")
.user(colloquialQuery)
.call()
.content();
}
}效果对比:
| 原始查询 | 扩展后查询 | 检索准确率 |
|---|---|---|
| "心梗用药" | "心梗 心肌梗死 MI AMI 用药 治疗" | +35% |
| "INR高了怎么办" | "INR高了怎么办 国际标准化比值 凝血功能 抗凝 华法林" | +42% |
| "血糖高的并发症" | "血糖高的并发症 糖尿病 DM diabetes mellitus HbA1c" | +28% |
八、知识更新机制:版本管理与增量更新
8.1 知识库版本管理
@Entity
@Table(name = "knowledge_versions")
public class KnowledgeVersion {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String documentId;
private String version; // 语义版本:1.2.3
private String checksum; // 文件MD5,用于检测变更
private LocalDate effectiveDate; // 生效日期
private LocalDate expiryDate; // 失效日期(null表示当前有效)
private String changeType; // ADDED/UPDATED/DEPRECATED
private String changeSummary; // 变更摘要
@Enumerated(EnumType.STRING)
private KnowledgeStatus status; // ACTIVE/SUPERSEDED/ARCHIVED
}@Service
public class KnowledgeUpdateService {
private final KnowledgeVersionRepository versionRepo;
private final VectorStore vectorStore;
private final DomainDocumentIngestionService ingestionService;
// 增量更新:只处理变更的文档
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点
public void incrementalUpdate() {
log.info("开始知识库增量更新检查");
List<DomainDocument> documents = documentRepo.findAll();
int updatedCount = 0;
for (DomainDocument doc : documents) {
String currentChecksum = calculateChecksum(doc.getFilePath());
KnowledgeVersion latestVersion = versionRepo.findLatestByDocumentId(doc.getId());
if (latestVersion == null || !latestVersion.getChecksum().equals(currentChecksum)) {
log.info("检测到文档变更: {}", doc.getTitle());
// 1. 标记旧版本为已废弃
if (latestVersion != null) {
latestVersion.setStatus(KnowledgeStatus.SUPERSEDED);
latestVersion.setExpiryDate(LocalDate.now());
versionRepo.save(latestVersion);
// 删除旧版本的向量
vectorStore.delete(
vectorStore.similaritySearch(
SearchRequest.query("").withFilterExpression(
"document_id == '" + doc.getId() + "'"
).withTopK(10000)
).stream().map(Document::getId).toList()
);
}
// 2. 摄入新版本
IngestionResult result = ingestionService.ingestDocument(doc);
// 3. 记录新版本
KnowledgeVersion newVersion = new KnowledgeVersion();
newVersion.setDocumentId(doc.getId());
newVersion.setVersion(incrementVersion(latestVersion));
newVersion.setChecksum(currentChecksum);
newVersion.setEffectiveDate(LocalDate.now());
newVersion.setStatus(KnowledgeStatus.ACTIVE);
newVersion.setChangeType(latestVersion == null ? "ADDED" : "UPDATED");
versionRepo.save(newVersion);
updatedCount++;
}
}
log.info("增量更新完成,共更新 {} 个文档", updatedCount);
}
private String incrementVersion(KnowledgeVersion prev) {
if (prev == null) return "1.0.0";
String[] parts = prev.getVersion().split("\\.");
return parts[0] + "." + parts[1] + "." + (Integer.parseInt(parts[2]) + 1);
}
}九、领域评估:如何测量注入效果
9.1 评估指标体系
9.2 自动化评估框架
@Service
public class DomainEvaluationService {
private final ChatClient judgeClient; // 用于自动评估的LLM
private final List<EvaluationCase> goldStandard; // 人工标注的标准答案
public EvaluationReport evaluate(RAGSystem ragSystem) {
List<CaseResult> results = new ArrayList<>();
for (EvaluationCase testCase : goldStandard) {
// 获取系统回答
String systemAnswer = ragSystem.query(testCase.getQuestion());
// 自动评估(使用强大的LLM作为judge)
EvaluationScore score = autoEvaluate(
testCase.getQuestion(),
testCase.getGoldAnswer(),
systemAnswer,
testCase.getCriteria()
);
results.add(CaseResult.builder()
.testCase(testCase)
.systemAnswer(systemAnswer)
.score(score)
.build());
}
// 汇总统计
DoubleSummaryStatistics accuracyStats = results.stream()
.mapToDouble(r -> r.getScore().getAccuracy())
.summaryStatistics();
DoubleSummaryStatistics completenessStats = results.stream()
.mapToDouble(r -> r.getScore().getCompleteness())
.summaryStatistics();
return EvaluationReport.builder()
.totalCases(results.size())
.avgAccuracy(accuracyStats.getAverage())
.avgCompleteness(completenessStats.getAverage())
.p90Accuracy(calculatePercentile(results, 90))
.failedCases(results.stream()
.filter(r -> r.getScore().getAccuracy() < 0.7)
.toList())
.build();
}
private EvaluationScore autoEvaluate(
String question, String goldAnswer,
String systemAnswer, List<String> criteria) {
String prompt = """
你是领域知识评估专家。请评估AI系统的回答质量。
问题:%s
标准答案:%s
系统回答:%s
评估标准:%s
请输出JSON格式的评分(0-1):
{
"accuracy": 0.0, // 事实准确性
"completeness": 0.0, // 信息完整性
"citation": 0.0, // 来源引用准确性
"safety": 0.0, // 安全合规性(不该说的没说)
"reasoning": "评分依据(一句话)"
}
""".formatted(question, goldAnswer, systemAnswer,
String.join("\n", criteria));
String response = judgeClient.prompt().user(prompt).call().content();
return parseEvaluationScore(response);
}
}十、案例:法律AI助手的完整领域知识注入实战
10.1 场景描述
某律所搭建智能合同审查系统,需要:
- 识别合同中的高风险条款
- 基于《民法典》给出修改建议
- 理解律所内部的审查标准
初始准确率:58%(专业律师评估)
10.2 完整实现
// 法律AI助手主服务
@Service
@Slf4j
public class LegalAIAssistant {
private final LegalRAGService ragService;
private final LegalPromptFactory promptFactory;
private final LegalTermExpander termExpander;
private final ContractRiskExtractor riskExtractor;
public ContractReviewResult reviewContract(
String contractText,
String contractType,
String clientSide) {
log.info("开始审查合同,类型: {}, 委托方: {}", contractType, clientSide);
// 阶段1:合同分段解析
List<ContractSection> sections = parseContractSections(contractText);
log.info("解析合同条款数: {}", sections.size());
// 阶段2:对每个条款进行风险评估
List<ClauseRisk> clauseRisks = new ArrayList<>();
for (ContractSection section : sections) {
ClauseRisk risk = analyzeClause(section, contractType, clientSide);
if (risk.getLevel() != RiskLevel.LOW) {
clauseRisks.add(risk);
}
}
// 阶段3:全局合同合规检查
List<ComplianceIssue> complianceIssues = checkOverallCompliance(
contractText, contractType
);
// 阶段4:生成修改建议
List<Amendment> amendments = generateAmendments(
clauseRisks, contractType, clientSide
);
return ContractReviewResult.builder()
.contractType(contractType)
.totalClauses(sections.size())
.highRiskClauses(clauseRisks.stream()
.filter(r -> r.getLevel() == RiskLevel.HIGH).count())
.mediumRiskClauses(clauseRisks.stream()
.filter(r -> r.getLevel() == RiskLevel.MEDIUM).count())
.clauseRisks(clauseRisks)
.complianceIssues(complianceIssues)
.amendments(amendments)
.overallRiskLevel(calculateOverallRisk(clauseRisks))
.build();
}
private ClauseRisk analyzeClause(
ContractSection section,
String contractType,
String clientSide) {
// 1. 从法律知识库检索相关规定
String legalContext = ragService.retrieveLegalContext(
section.getContent(), contractType
);
// 2. 构建专业提示词
String systemPrompt = promptFactory.buildContractReviewPrompt(
contractType, clientSide
);
// 3. 分析条款风险
String analysis = chatClient.prompt()
.system(systemPrompt)
.user(u -> u.text("""
请分析以下合同条款的法律风险:
条款内容:
{clause}
相关法律规定:
{legalContext}
请输出JSON:
{
"riskLevel": "HIGH|MEDIUM|LOW",
"riskType": "风险类型",
"legalBasis": "法律依据(具体条款)",
"description": "风险说明",
"suggestion": "修改建议"
}
""")
.param("clause", section.getContent())
.param("legalContext", legalContext))
.call()
.content();
return parseClauseRisk(analysis, section);
}
}10.3 效果对比
经过4周的领域知识注入改造后:
| 评估维度 | 改造前 | 改造后 | 提升 |
|---|---|---|---|
| 整体准确率 | 58% | 87% | +29% |
| 高风险识别召回率 | 45% | 91% | +46% |
| 法律依据引用准确率 | 62% | 94% | +32% |
| 修改建议可用率 | 40% | 82% | +42% |
| 专业律师满意度 | 3.1/5 | 4.4/5 | +42% |
十一、FAQ
Q:提示词注入和RAG应该怎么配合使用?
A:提示词注入放"规则",RAG放"事实"。提示词中写行为规范和输出格式要求,RAG负责检索具体的领域知识文档。两者互补,不互斥。
Q:向量数据库选哪个?
A:本地测试用Chroma,生产环境用Milvus(并发高)或PGVector(已有PostgreSQL的情况)。Pinecone是SaaS选择,省运维但有隐私顾虑。
Q:领域知识很多,上下文放不下怎么办?
A:这正是RAG解决的问题。把文档向量化,检索最相关的片段注入上下文。一般注入5-8个文档块(约2000-4000 tokens)效果最好,不是越多越好。
Q:微调后模型遗忘原来的知识怎么办?
A:这是"灾难性遗忘"问题。方案:(1)用LoRA轻量微调,只训练少量参数;(2)在微调数据中混入通用数据(比例约1:10);(3)使用持续学习技术(EWC算法)。
Q:如何评估领域知识注入的效果?
A:建立领域测试集(至少100道专业题),由领域专家标注标准答案,用自动化框架+人工审核双重验证。关键指标:事实准确率、来源引用准确率、危险错误率(零容忍指标)。
Q:RAG检索结果质量差怎么排查?
A:按优先级检查:1)Embedding模型是否适合领域(通用vs领域专用);2)文档分块策略是否合理(块太大信息稀释,块太小缺少上下文);3)元数据过滤是否设置;4)是否加了重排序。通常2-3步能定位问题。
总结
领域知识注入是AI应用从"通用玩具"到"专业工具"的关键升级路径:
- 提示词注入:最快,适合规则明确、数量有限的场景(30分钟见效)
- RAG注入:主力方案,适合大量文档、频繁更新(1-2周见效)
- Fine-tuning:深度定制,仅在前两者不足时考虑(1个月+见效)
- 知识图谱增强:RAG的升级版,解决关联推理问题
- 术语扩展:简单改造,检索准确率提升20-40%
不要追求一步到位,从最简单的提示词注入开始验证效果,再逐步升级。
