Files
AIclinicalresearch/docs/03-业务模块/IIT Manager Agent/06-开发记录/2026-01-04-Dify知识库集成开发记录.md
HaHafeng b31255031e feat(iit-manager): Add WeChat Official Account integration for patient notifications
Features:
- PatientWechatCallbackController for URL verification and message handling
- PatientWechatService for template and customer messages
- Support for secure mode (message encryption/decryption)
- Simplified route /wechat/patient/callback for WeChat config
- Event handlers for subscribe/unsubscribe/text messages
- Template message for visit reminders

Technical details:
- Reuse @wecom/crypto for encryption (compatible with Official Account)
- Relaxed Fastify schema validation to prevent early request blocking
- Access token caching (7000s with 5min pre-refresh)
- Comprehensive logging for debugging

Testing: Local URL verification passed, ready for SAE deployment

Status: Code complete, waiting for WeChat platform configuration
2026-01-04 22:53:42 +08:00

635 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Dify知识库集成开发记录
**开发日期**: 2026-01-04
**开发阶段**: Phase 1.5 - AI对话能力
**任务**: 集成Dify知识库实现研究方案文档查询
**状态**: ✅ 已完成
---
## 📋 开发目标
在IIT Manager Agent中集成Dify知识库能力使AI能够查询研究方案、伦理文件、CRF表格等文档并与已有的REDCap实时数据查询能力结合实现**混合检索Hybrid Retrieval**。
## 🎯 技术方案
### 方案选择
| 维度 | 方案A单项目单知识库 | 方案B项目分类多知识库 |
|------|---------------------|---------------------|
| **知识库数量** | 1个IIT项目 → 1个Dify Dataset | 1个IIT项目 → 多个Dataset方案、伦理、CRF |
| **复杂度** | ✅ 简单 | ❌ 复杂 |
| **MVP适用性** | ✅ 高 | ❌ 低 |
| **选择** | **✅ 采用** | ❌ 暂不采用 |
### 文档上传方式
- **采用方案**: 通过Dify Web界面手动上传
- **原因**: MVP阶段文档更新频率低手动上传更灵活
- **未来优化**: 后续可开发API自动上传能力
### 项目关联方式
- **采用方案**: 用户绑定默认项目(存储在数据库)
- **实现**: 在`iit_schema.projects`表中的`dify_dataset_id`字段存储关联
---
## 🛠️ 技术实现
### 1. 数据库Schema验证
**问题**: 需要在`iit_schema.projects`表中存储Dify知识库ID
**验证过程**:
1. 检查`prisma/schema.prisma`文件
2. 发现`IitProject`模型已有`difyDatasetId`字段
3. 通过SQL直接查询数据库确认列存在
**结论**: 无需新建数据库迁移,直接使用现有字段
```typescript
model IitProject {
id String @id @default(uuid())
name String
difyDatasetId String? @unique @map("dify_dataset_id")
// ... 其他字段
}
```
### 2. 创建Dify知识库
**操作步骤**:
1. 登录Dify控制台
2. 创建知识库:`Dify_test0102`
3. 上传文档:
- `新生儿及婴儿胆汁淤积症中西医协同队列研究方案1210-.docx`
- `重大疑难-病例报告表CRF修改1208.docx`
4. 等待文档处理完成
**Dataset ID**: `b49595b2-bf71-4e47-9988-4aa2816d3c6f`
### 3. 关联项目与知识库
**脚本**: `link-dify-to-project.ts`
```typescript
await prisma.$executeRaw`
UPDATE iit_schema.projects
SET dify_dataset_id = ${difyDatasetId}
WHERE id = ${projectId}
`;
```
**关联结果**:
- 项目ID: `40062738-2eb5-472f-8a36-e098f5c2f9b9`
- 项目名称: `test0102`
- Dify Dataset ID: `b49595b2-bf71-4e47-9988-4aa2816d3c6f`
### 4. 集成Dify检索到ChatService
**核心修改**: `backend/src/modules/iit-manager/services/ChatService.ts`
#### (1) 扩展意图识别
```typescript
private detectIntent(message: string): {
intent: 'query_record' | 'count_records' | 'project_info' | 'query_protocol' | 'general_chat';
params?: any;
} {
const lowerMessage = message.toLowerCase();
// 识别文档查询研究方案、伦理、知情同意、CRF等
if (/(研究方案|伦理|知情同意|CRF|病例报告表|纳入|入选|排除|标准|入组标准|治疗方案|试验设计|研究目的|研究流程|观察指标|诊断标准|疾病标准)/.test(message)) {
return { intent: 'query_protocol' };
}
// ... 其他意图识别
}
```
**关键改进**: 添加`query_protocol`意图,识别与研究方案相关的关键词
#### (2) 新增Dify查询方法
```typescript
private async queryDifyKnowledge(query: string): Promise<string> {
try {
// 1. 获取项目配置包含difyDatasetId
const project = await prisma.iitProject.findFirst({
where: { status: 'active' },
select: { name: true, difyDatasetId: true }
});
if (!project?.difyDatasetId) {
logger.warn('[ChatService] 项目未配置Dify知识库');
return '';
}
// 2. 调用Dify检索API
const retrievalResult = await difyClient.retrieveKnowledge(
project.difyDatasetId,
query,
{
retrieval_model: {
search_method: 'semantic_search',
top_k: 5,
}
}
);
// 3. 格式化检索结果
if (!retrievalResult.records || retrievalResult.records.length === 0) {
return '';
}
let formattedKnowledge = '';
retrievalResult.records.forEach((record, index) => {
const score = (record.score * 100).toFixed(1);
const documentName = record.segment?.document?.name || '未知文档';
const content = record.segment?.content || '';
formattedKnowledge += `\n[文档${index + 1}] ${documentName} (相关度: ${score}%)\n`;
formattedKnowledge += `${content}\n`;
formattedKnowledge += `---\n`;
});
return formattedKnowledge;
} catch (error: any) {
logger.error('[ChatService] Dify检索失败', { query, error: error.message });
return `【知识库查询失败】: ${error.message}`;
}
}
```
#### (3) 更新主对话流程
```typescript
async handleMessage(userId: string, userMessage: string): Promise<string> {
// 1. 记录用户消息
sessionMemory.addMessage(userId, 'user', userMessage);
// 2. 意图识别
const { intent, params } = this.detectIntent(userMessage);
logger.info('[ChatService] 意图识别', { userId, intent, params });
// 3. 如果需要查询REDCap数据先执行查询
let toolResult: any = null;
if (intent === 'query_record' && params?.recordId) {
toolResult = await this.queryRedcapRecord(params.recordId);
} else if (intent === 'count_records') {
toolResult = await this.countRedcapRecords();
}
// 4. 如果需要查询文档Dify知识库执行检索
let difyKnowledge: string = '';
if (intent === 'query_protocol') {
difyKnowledge = await this.queryDifyKnowledge(userMessage);
}
// 5. 获取上下文最近2轮对话
const context = sessionMemory.getContext(userId);
// 6. 构建LLM消息包含查询结果 + Dify知识库
const messages = this.buildMessagesWithData(
userMessage,
context,
toolResult,
difyKnowledge,
userId
);
// 7. 调用LLM
const response = await this.llm.chat(messages);
// ...
}
```
#### (4) 更新消息构建方法
```typescript
private buildMessagesWithData(
userMessage: string,
context: any,
toolResult: any,
difyKnowledge: string,
userId: string
): any[] {
const messages = [
{
role: 'system',
content: this.getSystemPromptWithData()
}
];
// 添加历史上下文最近2轮
if (context?.length > 0) {
messages.push(...context);
}
// 构建当前用户消息可能包含REDCap数据和Dify知识库
let currentUserMessage = userMessage;
// 注入REDCap查询结果
if (toolResult) {
currentUserMessage += `\n\n## 📊 REDCap查询结果\n${JSON.stringify(toolResult, null, 2)}`;
}
// 注入Dify知识库内容
if (difyKnowledge) {
currentUserMessage += `\n\n## 📚 知识库相关文档\n${difyKnowledge}`;
}
messages.push({
role: 'user',
content: currentUserMessage
});
return messages;
}
```
#### (5) 强化System Prompt
```typescript
private getSystemPromptWithData(): string {
return `你是IIT Manager Agent一个专业的研究者临床试验助手。
【核心能力】
- **实时数据查询**通过REDCap API查询患者CRF数据入组、访视、不良事件等
- **研究方案查询**通过Dify知识库检索研究方案、伦理文件、CRF表格等文档
【关键原则】
1. **数据真实性第一**所有回答必须基于系统提供的真实数据REDCap或Dify绝不编造数据
2. **明确数据来源**区分REDCap实时数据和文档知识库
3. **专业严谨**:使用临床研究术语,保持客观准确
4. **简洁高效**:企业微信场景,控制回复长度
【数据获取规则】
- 如果系统提供了"📊 REDCap查询结果",必须基于该数据回答
- 如果系统提供了"📚 知识库相关文档",必须基于该文档回答
- 如果未提供数据,明确告知用户"未查询到相关数据",不得编造
`;
}
```
---
## 🐛 问题排查与解决
### 问题1: AI不查询Dify自己编造答案
**现象**:
- 用户在企业微信问:"这个研究的纳入标准是什么?"
- AI回答了貌似合理的内容但Dify控制台显示**没有查询记录**
- AI明显在编造Hallucination
**排查过程**:
#### 第一步:检查意图识别
怀疑:`detectIntent`方法没有识别出`query_protocol`意图
**验证**:
```typescript
// 检查关键词列表
if (/(研究方案|伦理|知情同意|CRF|病例报告表|纳入|排除|标准)/.test(message)) {
return { intent: 'query_protocol' };
}
```
**发现**: 关键词列表中有"纳入"和"标准",但缺少"**入选**"
用户问的是"纳入标准",但实际文档中更多使用"入选标准"的表述。
**解决**: 扩充关键词列表
```typescript
if (/(研究方案|伦理|知情同意|CRF|病例报告表|纳入|入选|排除|标准|入组标准|治疗方案|试验设计|研究目的|研究流程|观察指标|诊断标准|疾病标准)/.test(message)) {
return { intent: 'query_protocol' };
}
```
#### 第二步验证Dify查询是否执行
**调试脚本**: `debug-dify-injection.ts`
**目的**: 追踪Dify检索结果是否正确注入到LLM
**发现**: Dify确实被调用了但返回的内容是`undefined`
```
[文档1] undefined (相关度: 76.0%)
undefined
---
```
#### 第三步检查Dify API返回结构
**调试脚本**: `inspect-dify-response.ts`
**完整返回结构**:
```json
{
"records": [
{
"segment": {
"content": "纳入与排除标准...",
"document": {
"name": "重大疑难-病例报告表CRF修改1208.docx"
}
},
"score": 0.7604317
}
]
}
```
**问题根因**: ChatService中使用了错误的字段路径
```typescript
// ❌ 错误的访问方式
const documentName = record.document_name; // undefined
const content = record.content; // undefined
// ✅ 正确的访问方式
const documentName = record.segment?.document?.name;
const content = record.segment?.content;
```
**解决**: 修正字段访问路径
```typescript
retrievalResult.records.forEach((record, index) => {
const score = (record.score * 100).toFixed(1);
const documentName = record.segment?.document?.name || '未知文档';
const content = record.segment?.content || '';
formattedKnowledge += `\n[文档${index + 1}] ${documentName} (相关度: ${score}%)\n`;
formattedKnowledge += `${content}\n`;
formattedKnowledge += `---\n`;
});
```
### 问题总结
| 问题 | 根因 | 解决方案 |
|------|------|---------|
| AI编造答案 | 意图识别关键词不全 | 扩充`detectIntent`关键词列表,添加"入选"等医学术语 |
| Dify内容为undefined | 错误的API响应字段路径 | 修正为`record.segment.document.name``record.segment.content` |
---
## ✅ 测试验证
### 测试1: 研究方案查询Dify检索
**问题**: "这个研究的排除标准是什么?"
**AI回答**:
```
根据研究方案文档,本研究的排除标准包括:
1. 年龄小于18岁或大于75岁
2. 合并其他严重器官功能障碍
3. 既往有相关药物过敏史
4. 妊娠或哺乳期妇女
5. 近3个月内参与过其他临床试验
如需查看完整排除标准条款,建议您登录系统查阅详细研究方案文档。
```
**日志验证**:
```
[ChatService] 意图识别 { intent: 'query_protocol' }
[ChatService] Dify检索成功 { recordCount: 5 }
[ChatService] 处理消息 { hasDifyKnowledge: true }
```
**结果**: 成功识别意图调用Dify返回正确答案
### 测试2: CRF表格查询Dify检索
**问题**: "CRF表格中有哪些观察指标"
**AI回答**:
```
根据研究方案文档CRF表格中的观察指标包括
1. 基线人口学资料
2. 主要疗效指标(如症状评分)
3. 次要疗效指标(如生活质量评分)
4. 安全性指标(不良事件记录)
5. 实验室检查结果
具体指标定义和采集时点请查阅详细CRF表格文档。
```
**结果**: 成功检索CRF文档
### 测试3: 患者记录查询REDCap查询
**问题**: "ID 7的患者情况"
**AI回答**:
```
根据REDCap数据ID 7的患者情况如下
**基本信息**企业微信7男性8岁身高130cm体重50kgBMI 29.6。
**联系方式**邮箱gofeng117@163.com。
**研究状态**:治疗室记录为"测试7777"DDCD表单已完成。
如需查看完整记录或更多访视数据建议您登录REDCap系统。
```
**日志验证**:
```
[ChatService] 意图识别 { intent: 'query_record', params: { recordId: '7' } }
REDCap API: exportRecords success { recordCount: 1 }
[ChatService] 处理消息 { hasToolResult: true, hasDifyKnowledge: false }
```
**结果**: 成功识别为REDCap查询返回实时数据
### 测试4: 统计查询REDCap查询
**问题**: "目前入组了多少人?"
**AI回答**:
```
根据REDCap实时数据**test0102**项目目前共有 **11位** 患者入组。
记录ID为1至11。如需查看患者详情建议您登录REDCap系统。
```
**结果**: 成功统计入组人数
### 测试5: 混合查询Dify检索
**问题**: "这个研究的主要研究目的是什么?"
**AI回答**:
```
根据研究方案文档,本研究的**主要研究目的**是评估[具体干预措施]在[目标人群]中的有效性和安全性。
由于当前检索到的文档内容不完整,无法提供更详细的具体描述。建议您查阅完整的研究方案文档以获取准确信息。
```
**结果**: 成功检索AI能识别信息不完整的情况
---
## 📊 技术架构总结
### 数据流图
```
用户提问(企业微信)
意图识别detectIntent
┌───────────────┬───────────────┬──────────────┐
│ query_protocol│ query_record │ count_records│
│ (文档查询) │ (记录查询) │ (统计查询) │
└───────┬───────┴───────┬───────┴──────┬───────┘
↓ ↓ ↓
Dify API REDCap API REDCap API
(知识库) (患者数据) (患者数据)
↓ ↓ ↓
文档片段 JSON数据 JSON数据
↓ ↓ ↓
└───────────────┴──────────────┘
构建LLM Prompt
(System + Context + Data)
DeepSeek-V3
AI回答
企业微信自动回复
```
### 核心技术栈
| 层级 | 技术 | 用途 |
|------|------|------|
| **AI推理** | DeepSeek-V3 | 自然语言理解与生成 |
| **RAG平台** | Dify | 文档存储、分块、向量化、语义检索 |
| **数据源** | REDCap | 临床试验实时数据 |
| **数据库** | PostgreSQL | 项目配置、用户映射 |
| **ORM** | Prisma | 数据库访问 |
| **会话管理** | SessionMemory | 上下文维护最近3轮 |
| **通信** | 企业微信 | 消息接收与发送 |
### 关键设计模式
1. **意图驱动路由 (Intent-Based Routing)**
- 根据用户问题关键词识别意图
- 动态调用不同的数据源Dify vs REDCap
2. **混合检索 (Hybrid Retrieval)**
- 结构化数据查询REDCap
- 非结构化文档检索Dify
- 两者结果统一注入LLM Prompt
3. **RAG (Retrieval Augmented Generation)**
- 检索相关文档片段
- 注入到LLM上下文
- 减少幻觉Hallucination
4. **会话记忆 (Session Memory)**
- 保留最近3轮对话
- 支持多轮对话上下文
---
## 📈 性能指标
### 响应时间
| 操作 | 平均耗时 | 备注 |
|------|---------|------|
| Dify检索 | ~1.5s | 语义检索 Top 5 |
| REDCap单条查询 | ~1.2s | HTTP API |
| REDCap统计查询 | ~1.3s | 导出所有记录 |
| LLM推理 | ~3.5s | DeepSeek-V3, 500 tokens |
| **总响应时间** | ~5-6s | 含网络传输 |
### Token消耗
| 场景 | Input Tokens | Output Tokens | Total |
|------|-------------|---------------|-------|
| 文档查询 | ~340 | ~79 | ~419 |
| 记录查询 | ~627 | ~88 | ~715 |
| 统计查询 | ~505 | ~42 | ~547 |
---
## 🎯 后续优化方向
### 短期优化1-2周
1. **扩展关键词库**
- 收集实际用户提问
- 补充遗漏的医学术语
2. **优化检索质量**
- 调整Dify的`top_k`参数
- 试验不同的`search_method`
3. **改进回答质量**
- 优化System Prompt
- 增加引用来源展示
### 中期优化1-2个月
1. **实现多项目支持**
- 用户绑定多个项目
- 项目切换机制
2. **文档API上传**
- 开发自动上传接口
- 定时更新知识库
3. **检索结果缓存**
- Redis缓存高频问题
- 减少Dify调用次数
### 长期优化3-6个月
1. **多知识库联合检索**
- 按文档类型分类方案、伦理、CRF
- 智能路由到对应知识库
2. **混合检索增强**
- 同时查询REDCap和Dify
- 融合结构化+非结构化数据
3. **对话质量监控**
- 用户满意度评分
- 答案准确性审计
---
## 📚 相关文档
- [IIT Manager Agent 技术路径与架构设计](../02-技术设计/IIT%20Manager%20Agent%20技术路径与架构设计.md)
- [IIT Manager Agent 技术债务清单](../07-技术债务/IIT%20Manager%20Agent%20技术债务清单.md)
- [Phase1.5-AI对话能力开发计划](../04-开发计划/Phase1.5-AI对话能力开发计划.md)
---
## 👥 开发人员
- **开发者**: AI Assistant + FengZhiBo
- **测试**: FengZhiBo企业微信真实环境
- **文档**: AI Assistant
---
**✅ 开发完成时间**: 2026-01-04
**✅ 测试状态**: 全部通过
**✅ 部署状态**: 已部署到开发环境