Completed features: - Created Dify dataset (Dify_test0102) with 2 processed documents - Linked test0102 project with Dify dataset ID - Extended intent detection to recognize query_protocol intent - Implemented queryDifyKnowledge method (semantic search Top 5) - Integrated hybrid retrieval (REDCap data + Dify documents) - Fixed AI hallucination bugs (intent detection + API field path) - Developed debugging scripts - Completed end-to-end testing (5 scenarios passed) - Generated comprehensive documentation (600+ lines) - Updated development plans and module status Technical highlights: - Single project single knowledge base architecture - Smart routing based on user intent - Prevent AI hallucination by injecting real data/documents - Session memory for multi-turn conversations - Reused LLMFactory for DeepSeek-V3 integration Bug fixes: - Fixed intent detection missing keywords - Fixed Dify API response field path error Testing: All scenarios verified in WeChat production environment Status: Fully tested and deployed
84 KiB
IIT Manager Agent - Phase 1.5 AI对话能力开发计划
版本: v3.0(极简版 + 上下文记忆 + Dify知识库)
创建日期: 2026-01-03
最新更新: 2026-01-04
状态: ✅ 已完成(含Dify集成)
实际工作量: ~2天(极简版 + Dify知识库)
核心价值: PI可在企业微信中自然对话查询REDCap真实数据 + 研究方案文档
核心成就: ✅ REDCap数据集成 + ✅ 上下文记忆 + ✅ 解决LLM幻觉 + ✅ Dify知识库混合检索
🚀 极简版快速启动(1天上线)⚡ 通用能力层加速!
🎉 重大发现:通用能力层已完善!
平台现状(2026-01-03调研结果):
- ✅ LLMFactory 完全就绪:5种模型(DeepSeek/Qwen/GPT-5/Claude),单例模式,零配置
- ✅ ChatContainer 完全就绪:Ant Design X组件,已在Tool C验证(~968行)
- ✅ 环境变量已配置:
DEEPSEEK_API_KEY、QWEN_API_KEY等 - ✅ 成熟实践:ASL、DC模块已大量使用,稳定可靠
核心优势:
// 后端LLM调用(3行代码)
import { LLMFactory } from '@/common/llm/adapters/LLMFactory.js';
const llm = LLMFactory.getAdapter('deepseek-v3');
const response = await llm.chat(messages, { temperature: 0.7 });
极简版功能范围
✅ Day 1(4-6小时): 实现基础对话 + 上下文记忆 + "typing"反馈
- 复用LLMFactory(0开发)
- 创建ChatService.ts(2小时)
- 创建SessionMemory.ts(2小时)
- 修改WechatCallbackController(2小时)
✅ Day 2(4-6小时): Dify知识库集成 + 混合检索(2026-01-04完成)
- 关联项目与Dify知识库(1小时)
- 集成Dify检索到ChatService(2小时)
- 修复意图识别与数据注入bug(2小时)
- 端到端测试与文档记录(1小时)
❌ 暂不实现: 周报生成、复杂Tool Calling
极简版架构(复用通用能力层)
PI提问 → Node.js → LLMFactory(deepseek-v3) → 生成回答 → 企业微信推送
↓ ↑
SessionMemory RedcapAdapter(可选)
关键决策:
- 🚀 复用LLMFactory(已有,零开发,推荐
deepseek-v3) - ✅ 只查REDCap数据(已有RedcapAdapter,复用即可)
- ✅ 不接Dify(减少依赖,加快开发)
- ✅ 上下文记忆(Node.js内存,存最近3轮)
- ✅ 正在输入反馈(立即回"正在查询...")
- ✅ 单步路由(不用ReAct循环)
预估工作量大幅降低:2-3天 → 1天(因为LLM调用层已完善)⚡
🎯 一、核心目标与价值
1.1 为什么需要AI对话能力?
当前状态(Day 3已完成):
✅ REDCap录入数据 → Node.js捕获 → 企业微信推送通知
目标状态(Phase 1.5):
✅ PI在企业微信中提问 → AI理解意图 → 查询数据/文档 → 智能回答
核心价值:
- 🚀 主动查询:PI不用等通知,随时问"现在入组多少人?"
- 📊 数据穿透:实时查询REDCap数据(患者详情、质控状态)
- 📚 知识检索:查询研究方案、CRF表格、入排标准
- 💡 智能理解:自然语言提问,无需记忆命令
📊 二、技术架构(基于本地Dify)
2.1 整体架构图
┌─────────────────────────────────────────────────┐
│ PI (企业微信) │
│ "P001患者符合入排标准吗?" │
└───────────┬─────────────────────────────────────┘
│
↓
┌─────────────────────────────────────────────────┐
│ Node.js 后端 (已有 WechatCallbackController) │
│ ┌──────────────────────────────────────────┐ │
│ │ 1. 接收消息 (handleCallback) │ │
│ │ 2. 意图识别 (Intent Router) │ │
│ │ 3. 工具调用 (Tool Executor) │ │
│ │ 4. 企业微信推送 (WechatService) │ │
│ └──────────────────────────────────────────┘ │
└───────────┬─────────────────────────────────────┘
│
┌────┴────────┬───────────────┐
↓ ↓ ↓
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Dify │ │ REDCap │ │ PostgreSQL │
│ (本地Docker)│ │ API │ │ 数据库 │
│ │ │ │ │ │
│ - 研究方案 │ │ - 患者数据 │ │ - 项目配置 │
│ - CRF表格 │ │ - 质控状态 │ │ - 审计日志 │
│ - 周报归档 │ │ - 不良反应 │ │ - 用户映射 │
└──────────────┘ └──────────────┘ └──────────────┘
<50ms <100ms <20ms
本地调用 本地调用 本地调用
2.2 关键技术决策
| 决策点 | 方案 | 原因 |
|---|---|---|
| AI推理引擎 | DeepSeek-V3 (API) | 性价比高,支持Function Calling |
| 知识库 | Dify本地Docker | 已部署,无额外成本,延迟低 |
| 向量数据库 | Dify内置Weaviate | 免维护,开箱即用 |
| 路由策略 | 单步意图识别 | MVP阶段简化,不用ReAct循环 |
| 数据查询 | RedcapAdapter | 已有,直接复用 |
⚡ 二、极简版开发计划(2天)
🎯 Day 1:基础对话能力(6小时)
核心目标
让AI能回答用户问题(只查REDCap数据)
任务1.1:创建SessionMemory(30分钟)
文件位置:backend/src/modules/iit-manager/agents/SessionMemory.ts
/**
* 会话记忆管理器(内存版)
* 存储用户最近3轮对话,用于上下文理解
*/
export class SessionMemory {
// 内存存储:{ userId: ConversationHistory }
private sessions: Map<string, ConversationHistory> = new Map();
private readonly MAX_HISTORY = 3; // 只保留最近3轮
/**
* 添加对话记录
*/
addMessage(userId: string, role: 'user' | 'assistant', content: string): void {
if (!this.sessions.has(userId)) {
this.sessions.set(userId, {
userId,
messages: [],
createdAt: new Date(),
updatedAt: new Date()
});
}
const session = this.sessions.get(userId)!;
session.messages.push({
role,
content,
timestamp: new Date()
});
// 只保留最近3轮(6条消息)
if (session.messages.length > this.MAX_HISTORY * 2) {
session.messages = session.messages.slice(-this.MAX_HISTORY * 2);
}
session.updatedAt = new Date();
}
/**
* 获取用户对话历史
*/
getHistory(userId: string): ConversationMessage[] {
const session = this.sessions.get(userId);
return session?.messages || [];
}
/**
* 获取用户上下文(最近一轮)
*/
getContext(userId: string): string {
const history = this.getHistory(userId);
if (history.length === 0) return '';
// 只取最近一轮对话
const recentMessages = history.slice(-2);
return recentMessages
.map(m => `${m.role}: ${m.content}`)
.join('\n');
}
/**
* 清除用户会话
*/
clearSession(userId: string): void {
this.sessions.delete(userId);
}
/**
* 清理过期会话(超过1小时未使用)
*/
cleanupExpiredSessions(): void {
const now = Date.now();
const ONE_HOUR = 3600000;
for (const [userId, session] of this.sessions.entries()) {
if (now - session.updatedAt.getTime() > ONE_HOUR) {
this.sessions.delete(userId);
}
}
}
}
interface ConversationHistory {
userId: string;
messages: ConversationMessage[];
createdAt: Date;
updatedAt: Date;
}
interface ConversationMessage {
role: 'user' | 'assistant';
content: string;
timestamp: Date;
}
// 全局单例
export const sessionMemory = new SessionMemory();
// 定时清理过期会话(每小时)
setInterval(() => {
sessionMemory.cleanupExpiredSessions();
}, 3600000);
验收标准:
- ✅ 可存储对话历史
- ✅ 可获取上下文
- ✅ 自动清理过期会话
任务1.2:创建ChatService(2小时)⚡ 复用LLMFactory
文件位置:backend/src/modules/iit-manager/services/ChatService.ts
import { LLMFactory } from '../../../common/llm/adapters/LLMFactory.js';
import { Message } from '../../../common/llm/adapters/types.js';
import { logger } from '../../../common/logging/index.js';
import { sessionMemory } from '../agents/SessionMemory.js';
/**
* AI对话服务(复用通用能力层LLMFactory)
* 处理企业微信用户消息,支持上下文记忆
*/
export class ChatService {
private llm;
constructor() {
// ⚡ 复用通用能力层LLMFactory(零配置)
this.llm = LLMFactory.getAdapter('deepseek-v3');
}
/**
* 识别用户意图(带上下文)
*/
async route(
userMessage: string,
userId: string,
projectId: string
): Promise<IntentRouteResult> {
try {
// 1. 获取上下文
const context = sessionMemory.getContext(userId);
logger.info('[SimpleIntentRouter] Routing with context', {
message: userMessage.substring(0, 50),
hasContext: !!context,
userId
});
// 2. 构建Prompt(包含上下文)
const systemPrompt = this.buildSystemPrompt();
const userPrompt = context
? `【上下文】\n${context}\n\n【当前问题】\n${userMessage}`
: userMessage;
// 3. 调用LLM
const response = await this.llm.chat.completions.create({
model: 'deepseek-chat',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt }
],
tools: [this.getDataQueryTool()],
tool_choice: 'auto',
temperature: 0.1,
max_tokens: 500
});
const message = response.choices[0].message;
// 4. 如果LLM决定调用工具
if (message.tool_calls && message.tool_calls.length > 0) {
const toolCall = message.tool_calls[0];
const toolArgs = JSON.parse(toolCall.function.arguments);
// ✅ 上下文解析:如果args中有代词,尝试从上下文中解析
if (context && this.hasPronouns(userMessage)) {
toolArgs.patient_id = this.extractPatientIdFromContext(context, toolArgs);
}
return {
needsToolCall: true,
toolName: 'query_clinical_data',
toolArgs,
rawResponse: message
};
}
// 5. 直接回答
return {
needsToolCall: false,
directAnswer: message.content || '抱歉,我没有理解您的问题',
rawResponse: message
};
} catch (error: any) {
logger.error('[SimpleIntentRouter] Routing failed', {
error: error.message
});
return {
needsToolCall: false,
directAnswer: '抱歉,我遇到了一些问题,请稍后再试',
error: error.message
};
}
}
/**
* 构建System Prompt
*/
private buildSystemPrompt(): string {
return `# 角色
你是临床研究项目助手,帮助PI查询项目数据。
# 能力
你可以查询REDCap数据库,包括:
1. 项目统计(入组人数、数据完整率)
2. 患者详情(录入情况、基本信息)
3. 质控状态(数据问题)
# 上下文理解
- 如果用户说"他"、"这个患者"等代词,请根据【上下文】中提到的患者编号
- 如果上下文中没有患者编号,请要求用户提供
# 约束
- 严禁编造数据
- 只能查询REDCap数据,不能查询文档
- 回答要简洁专业`;
}
/**
* 定义数据查询工具
*/
private getDataQueryTool(): any {
return {
type: "function",
function: {
name: "query_clinical_data",
description: `查询REDCap临床数据。
适用场景:
- 问项目统计:现在入组多少人?数据质量如何?
- 问患者详情:P001患者录完了吗?有不良反应吗?
- 问质控状态:有哪些质控问题?`,
parameters: {
type: "object",
properties: {
intent: {
type: "string",
enum: ["project_stats", "patient_detail", "qc_status"],
description: `查询意图:
- project_stats: 项目统计
- patient_detail: 患者详情
- qc_status: 质控状态`
},
patient_id: {
type: "string",
description: "患者编号(如P001),当intent=patient_detail时必填"
}
},
required: ["intent"]
}
}
};
}
/**
* 检查消息中是否有代词
*/
private hasPronouns(message: string): boolean {
const pronouns = ['他', '她', '这个患者', '该患者', '这位', '那位'];
return pronouns.some(p => message.includes(p));
}
/**
* 从上下文中提取患者ID
*/
private extractPatientIdFromContext(context: string, toolArgs: any): string {
// 简单正则提取患者编号
const match = context.match(/P\d{3,}/);
return match ? match[0] : toolArgs.patient_id;
}
}
export interface IntentRouteResult {
needsToolCall: boolean;
toolName?: string;
toolArgs?: any;
directAnswer?: string;
error?: string;
rawResponse?: any;
}
验收标准:
- ✅ 可识别查询意图
- ✅ 支持上下文理解(代词解析)
- ✅ 错误处理完善
任务1.3:简化ToolExecutor(1.5小时)
文件位置:backend/src/modules/iit-manager/agents/SimpleToolExecutor.ts
import { RedcapAdapter } from '../adapters/RedcapAdapter.js';
import { logger } from '../../../common/logging/index.js';
import { prisma } from '../../../config/database.js';
/**
* 简化版工具执行器
* 只执行REDCap数据查询
*/
export class SimpleToolExecutor {
/**
* 执行查询临床数据
*/
async execute(
toolArgs: {
intent: 'project_stats' | 'patient_detail' | 'qc_status';
patient_id?: string;
},
context: {
projectId: string;
userId: string;
}
): Promise<ToolExecutionResult> {
try {
logger.info('[SimpleToolExecutor] Executing query', {
intent: toolArgs.intent,
patientId: toolArgs.patient_id,
projectId: context.projectId
});
// 1. 获取项目配置
const project = await prisma.iitProject.findUnique({
where: { id: context.projectId },
select: {
name: true,
redcapApiUrl: true,
redcapApiToken: true
}
});
if (!project) {
return {
success: false,
data: null,
error: '项目不存在'
};
}
// 2. 初始化RedcapAdapter
const redcap = new RedcapAdapter(
project.redcapApiUrl,
project.redcapApiToken
);
// 3. 根据intent执行查询
switch (toolArgs.intent) {
case 'project_stats':
return await this.getProjectStats(redcap, project.name);
case 'patient_detail':
if (!toolArgs.patient_id) {
return {
success: false,
data: null,
error: '请提供患者编号(如:P001)'
};
}
return await this.getPatientDetail(redcap, toolArgs.patient_id);
case 'qc_status':
return await this.getQCStatus(context.projectId);
default:
return {
success: false,
data: null,
error: '未知的查询类型'
};
}
} catch (error: any) {
logger.error('[SimpleToolExecutor] Execution failed', {
error: error.message
});
return {
success: false,
data: null,
error: error.message
};
}
}
/**
* 获取项目统计
*/
private async getProjectStats(
redcap: RedcapAdapter,
projectName: string
): Promise<ToolExecutionResult> {
const records = await redcap.exportRecords();
return {
success: true,
data: {
type: 'project_stats',
projectName,
stats: {
totalRecords: records.length,
enrolled: records.length,
completed: records.filter((r: any) => r.complete === '2').length,
dataQuality: this.calculateDataQuality(records)
}
}
};
}
/**
* 获取患者详情
*/
private async getPatientDetail(
redcap: RedcapAdapter,
patientId: string
): Promise<ToolExecutionResult> {
const records = await redcap.exportRecords([patientId]);
if (records.length === 0) {
return {
success: false,
data: null,
error: `未找到患者 ${patientId}`
};
}
const record = records[0];
return {
success: true,
data: {
type: 'patient_detail',
patientId,
details: {
age: record.age,
gender: record.gender,
bmi: record.bmi,
complete: record.complete === '2' ? '已完成' : '进行中',
lastUpdate: new Date().toISOString()
}
}
};
}
/**
* 获取质控状态
*/
private async getQCStatus(projectId: string): Promise<ToolExecutionResult> {
const logs = await prisma.iitAuditLog.findMany({
where: {
projectId,
actionType: 'quality_issue'
},
orderBy: { createdAt: 'desc' },
take: 10
});
return {
success: true,
data: {
type: 'qc_status',
issueCount: logs.length,
recentIssues: logs.map(log => ({
recordId: log.entityId,
issue: log.details,
createdAt: log.createdAt
}))
}
};
}
/**
* 计算数据质量(简单算法)
*/
private calculateDataQuality(records: any[]): string {
if (records.length === 0) return '0%';
const completedCount = records.filter((r: any) => r.complete === '2').length;
const quality = (completedCount / records.length) * 100;
return `${quality.toFixed(1)}%`;
}
}
export interface ToolExecutionResult {
success: boolean;
data: any;
error?: string;
}
验收标准:
- ✅ 可查询项目统计
- ✅ 可查询患者详情
- ✅ 可查询质控状态
任务1.4:简化AnswerGenerator(1小时)
文件位置:backend/src/modules/iit-manager/agents/SimpleAnswerGenerator.ts
import { ToolExecutionResult } from './SimpleToolExecutor.js';
import { logger } from '../../../common/logging/index.js';
/**
* 简化版答案生成器
* 使用模板生成回答,不调用LLM(节省成本)
*/
export class SimpleAnswerGenerator {
/**
* 生成回答
*/
generate(
userQuestion: string,
toolResult: ToolExecutionResult
): string {
try {
logger.info('[SimpleAnswerGenerator] Generating answer', {
success: toolResult.success,
dataType: toolResult.data?.type
});
// 如果工具执行失败
if (!toolResult.success) {
return this.generateErrorMessage(toolResult.error);
}
// 根据数据类型生成回答
const dataType = toolResult.data.type;
if (dataType === 'project_stats') {
return this.generateProjectStatsAnswer(toolResult.data);
} else if (dataType === 'patient_detail') {
return this.generatePatientDetailAnswer(toolResult.data);
} else if (dataType === 'qc_status') {
return this.generateQCStatusAnswer(toolResult.data);
}
return '抱歉,我无法生成回答';
} catch (error: any) {
logger.error('[SimpleAnswerGenerator] Generation failed', {
error: error.message
});
return '抱歉,回答生成失败';
}
}
/**
* 生成项目统计回答
*/
private generateProjectStatsAnswer(data: any): string {
const stats = data.stats;
return `📊 **${data.projectName}项目统计**
✅ **入组人数**:${stats.enrolled}例
✅ **完成病例**:${stats.completed}例
✅ **数据质量**:${stats.dataQuality}
💡 更新时间:${new Date().toLocaleString('zh-CN')}`;
}
/**
* 生成患者详情回答
*/
private generatePatientDetailAnswer(data: any): string {
const details = data.details;
return `👤 **患者 ${data.patientId} 详情**
📋 **基本信息**:
- 年龄:${details.age || '未录入'}岁
- 性别:${details.gender || '未录入'}
- BMI:${details.bmi || '未录入'}
📊 **录入状态**:
- ${details.complete}
💡 最后更新:${new Date().toLocaleString('zh-CN')}`;
}
/**
* 生成质控状态回答
*/
private generateQCStatusAnswer(data: any): string {
const issues = data.recentIssues.slice(0, 5);
let answer = `🔍 **质控状态**\n\n`;
answer += `⚠️ **质控问题数**:${data.issueCount}个\n\n`;
if (issues.length > 0) {
answer += `📋 **最近问题**:\n`;
issues.forEach((issue: any, index: number) => {
answer += `${index + 1}. 记录${issue.recordId}\n`;
});
} else {
answer += `✅ 暂无质控问题`;
}
return answer;
}
/**
* 生成错误提示
*/
private generateErrorMessage(error?: string): string {
return `❌ 查询失败
原因:${error || '未知错误'}
💡 您可以:
1. 稍后重试
2. 换个问法
3. 联系管理员`;
}
}
验收标准:
- ✅ 回答格式友好
- ✅ 支持Markdown
- ✅ 错误提示清晰
任务1.5:集成到WechatCallbackController(1小时)
修改文件:backend/src/modules/iit-manager/controllers/WechatCallbackController.ts
在handleCallback方法中添加:
import { sessionMemory } from '../agents/SessionMemory.js';
import { SimpleIntentRouter } from '../agents/SimpleIntentRouter.js';
import { SimpleToolExecutor } from '../agents/SimpleToolExecutor.js';
import { SimpleAnswerGenerator } from '../agents/SimpleAnswerGenerator.js';
class WechatCallbackController {
private intentRouter: SimpleIntentRouter;
private toolExecutor: SimpleToolExecutor;
private answerGenerator: SimpleAnswerGenerator;
constructor() {
// ... 现有代码 ...
this.intentRouter = new SimpleIntentRouter();
this.toolExecutor = new SimpleToolExecutor();
this.answerGenerator = new SimpleAnswerGenerator();
}
async handleCallback(request: FastifyRequest, reply: FastifyReply): Promise<void> {
// ... 现有的验证、解密逻辑 ...
// ✅ 立即返回success
reply.send('success');
// ✅ 异步处理(新增AI对话)
setImmediate(async () => {
try {
const userMessage = decryptedData.Content;
const userId = decryptedData.FromUserName;
logger.info('📥 收到用户消息', {
userId,
message: userMessage.substring(0, 50)
});
// ✅ 立即发送"正在查询..."反馈
await wechatService.sendTextMessage(
userId,
'🫡 正在查询,请稍候...'
);
// 1. 保存用户消息到会话记忆
sessionMemory.addMessage(userId, 'user', userMessage);
// 2. 获取用户的项目信息
const userMapping = await prisma.iitUserMapping.findFirst({
where: { wechatUserId: userId }
});
if (!userMapping) {
await wechatService.sendTextMessage(
userId,
'⚠️ 您还未绑定项目,请联系管理员配置'
);
return;
}
// 3. 意图识别(带上下文)
const routeResult = await this.intentRouter.route(
userMessage,
userId,
userMapping.projectId
);
// 4. 如果直接回答
if (!routeResult.needsToolCall) {
const answer = routeResult.directAnswer!;
await wechatService.sendTextMessage(userId, answer);
sessionMemory.addMessage(userId, 'assistant', answer);
return;
}
// 5. 执行工具
const toolResult = await this.toolExecutor.execute(
routeResult.toolArgs,
{
projectId: userMapping.projectId,
userId
}
);
// 6. 生成回答
const answer = this.answerGenerator.generate(userMessage, toolResult);
// 7. 发送回复
await wechatService.sendMarkdownMessage(userId, answer);
// 8. 保存AI回答到会话记忆
sessionMemory.addMessage(userId, 'assistant', answer);
// 9. 记录审计日志
await prisma.iitAuditLog.create({
data: {
projectId: userMapping.projectId,
actionType: 'wechat_user_query',
operator: userId,
entityId: userId,
details: {
question: userMessage,
answer: answer.substring(0, 200),
toolUsed: 'query_clinical_data',
hasContext: !!sessionMemory.getContext(userId)
}
}
});
logger.info('✅ 回答发送成功', { userId });
} catch (error: any) {
logger.error('❌ 处理用户消息失败', {
error: error.message
});
await wechatService.sendTextMessage(
userId,
'抱歉,我遇到了一些问题,请稍后再试'
);
}
});
}
}
验收标准:
- ✅ 可接收用户消息
- ✅ 立即发送"正在查询..."
- ✅ 正确识别意图
- ✅ 正确执行工具
- ✅ 正确发送回复
- ✅ 上下文记忆生效
🎯 Day 2:上下文优化 + 测试(4小时)
任务2.1:上下文记忆优化(1小时)
增强SessionMemory,支持患者ID提取:
// 在SessionMemory中添加
/**
* 从历史记录中提取最近提到的患者ID
*/
getLastPatientId(userId: string): string | null {
const history = this.getHistory(userId);
// 从最近的对话中倒序查找患者ID
for (let i = history.length - 1; i >= 0; i--) {
const message = history[i];
const match = message.content.match(/P\d{3,}/);
if (match) {
return match[0];
}
}
return null;
}
在SimpleIntentRouter中使用:
// 如果用户说"他有不良反应吗?",自动填充patient_id
if (context && this.hasPronouns(userMessage) && !toolArgs.patient_id) {
const lastPatientId = sessionMemory.getLastPatientId(userId);
if (lastPatientId) {
toolArgs.patient_id = lastPatientId;
logger.info('[SimpleIntentRouter] 自动填充患者ID', {
patientId: lastPatientId
});
}
}
任务2.2:完整测试(3小时)
测试场景:
// 场景1:无上下文查询
{
input: "现在入组多少人?",
expectedIntent: "project_stats",
expectedOutput: "📊 项目统计\n✅ 入组人数:XX例"
}
// 场景2:有上下文的多轮对话(关键!)
{
conversation: [
{
input: "帮我查一下P001的情况",
expectedIntent: "patient_detail",
expectedPatientId: "P001"
},
{
input: "他有不良反应吗?", // ← 代词"他"
expectedIntent: "patient_detail",
expectedPatientId: "P001", // ← 自动填充
expectedOutput: "应该包含P001"
}
]
}
// 场景3:正在输入反馈
{
input: "现在入组多少人?",
expectedFirstReply: "🫡 正在查询,请稍候...",
expectedSecondReply: "📊 项目统计..."
}
// 场景4:质控查询
{
input: "有哪些质控问题?",
expectedIntent: "qc_status",
expectedOutput: "🔍 质控状态"
}
// 场景5:闲聊
{
input: "你好",
expectedOutput: "您好!我是临床研究助手"
}
测试步骤:
- 在企业微信中发送测试消息
- 验证是否收到"正在查询..."
- 验证最终回复内容
- 检查审计日志中的上下文标记
- 测试多轮对话的上下文理解
验收标准:
- ✅ 5个测试场景全部通过
- ✅ "正在输入"反馈生效
- ✅ 上下文记忆生效(代词解析)
- ✅ 回复时间<3秒
📊 极简版成功标准
| 功能 | 验收标准 | 优先级 |
|---|---|---|
| 基础对话 | 可查询REDCap数据 | 🔴 P0 |
| 上下文记忆 | 支持最近3轮对话 | 🔴 P0 |
| 代词解析 | "他"能自动识别患者 | 🔴 P0 |
| 正在输入反馈 | 立即回"正在查询..." | 🔴 P0 |
| 回复延迟 | <3秒 | 🔴 P0 |
| 意图识别准确率 | >80% | 🔴 P0 |
🎉 极简版vs完整版对比
| 功能 | 极简版 (2天) | 完整版 (5天) |
|---|---|---|
| REDCap查询 | ✅ | ✅ |
| 上下文记忆 | ✅ (内存3轮) | ✅ (内存3轮) |
| 正在输入反馈 | ✅ | ✅ |
| Dify知识库 | ❌ | ✅ |
| 周报自动归档 | ❌ | ✅ |
| 文档查询 | ❌ | ✅ |
🗓️ 三、完整版开发计划(5天,可选)
Day 1:Dify环境配置与知识库创建(8小时)
任务1.1:验证Dify本地环境(1小时)
检查项:
# 1. 检查Dify容器状态
cd AIclinicalresearch/docker
docker-compose ps | grep dify
# 2. 访问Dify管理后台
# http://localhost/dify (或实际端口)
# 3. 获取API密钥
# Dify后台 → 设置 → API Keys → 创建
验收标准:
- ✅ Dify容器运行正常
- ✅ 可访问管理后台
- ✅ 获得API Key
任务1.2:创建IIT Manager知识库(2小时)
操作步骤:
-
创建知识库(Dify后台操作)
名称:IIT Manager - test0102项目 类型:通用知识库 Embedding模型:text-embedding-3-small (OpenAI) 分块策略:智能分块(500字符/块,重叠50字符) -
上传测试文档
- 上传1份CRF表格(PDF/Word)
- 上传1份入排标准文档(Markdown/Text)
- 上传1份研究方案摘要(PDF)
-
测试检索效果
测试问题1:"入组标准有哪些?" 测试问题2:"CRF表格中有哪些字段?" 测试问题3:"研究终点是什么?"
验收标准:
- ✅ 知识库创建成功
- ✅ 3份文档上传成功
- ✅ 检索测试准确率>80%
产出:
- Dify知识库ID
- API调用示例代码
任务1.3:实现Dify API适配器(3小时)
文件位置:backend/src/modules/iit-manager/adapters/DifyAdapter.ts
代码实现:
import axios from 'axios';
import { logger } from '../../../common/logging/index.js';
/**
* Dify API适配器
* 用于与本地Dify Docker实例交互
*/
export class DifyAdapter {
private baseUrl: string;
private apiKey: string;
private knowledgeBaseId: string;
constructor(projectId: string) {
// 从环境变量或数据库读取配置
this.baseUrl = process.env.DIFY_API_URL || 'http://localhost/v1';
this.apiKey = process.env.DIFY_API_KEY || '';
this.knowledgeBaseId = this.getKnowledgeBaseId(projectId);
}
/**
* 搜索知识库
* @param query 查询问题
* @param options 搜索选项
*/
async searchKnowledge(
query: string,
options?: {
doc_type?: 'protocol' | 'crf' | 'report';
top_k?: number;
}
): Promise<DifySearchResult> {
try {
logger.info('[DifyAdapter] Searching knowledge base', {
query,
options,
knowledgeBaseId: this.knowledgeBaseId
});
const response = await axios.post(
`${this.baseUrl}/datasets/${this.knowledgeBaseId}/retrieve`,
{
query: query,
retrieval_model: {
search_method: 'semantic_search',
top_k: options?.top_k || 3,
score_threshold: 0.5
},
// 如果指定了doc_type,通过metadata过滤
...(options?.doc_type && {
retrieval_model: {
filter: {
doc_type: options.doc_type
}
}
})
},
{
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
timeout: 10000 // 10秒超时
}
);
logger.info('[DifyAdapter] Search completed', {
recordCount: response.data.records?.length || 0
});
return {
success: true,
records: response.data.records || [],
query: query
};
} catch (error: any) {
logger.error('[DifyAdapter] Search failed', {
error: error.message,
query
});
return {
success: false,
records: [],
query,
error: error.message
};
}
}
/**
* 上传文档到知识库
* @param content 文档内容
* @param metadata 元数据
*/
async uploadDocument(
content: string,
metadata: {
name: string;
doc_type: 'protocol' | 'crf' | 'report';
date?: string;
}
): Promise<{ success: boolean; documentId?: string }> {
try {
logger.info('[DifyAdapter] Uploading document', {
name: metadata.name,
type: metadata.doc_type
});
const response = await axios.post(
`${this.baseUrl}/datasets/${this.knowledgeBaseId}/document/create_by_text`,
{
name: metadata.name,
text: content,
indexing_technique: 'high_quality',
process_rule: {
mode: 'automatic',
rules: {
pre_processing_rules: [
{ id: 'remove_extra_spaces', enabled: true },
{ id: 'remove_urls_emails', enabled: false }
],
segmentation: {
separator: '\n',
max_tokens: 500
}
}
},
doc_form: 'text_model',
doc_language: 'Chinese',
// 保存元数据
metadata: {
doc_type: metadata.doc_type,
date: metadata.date || new Date().toISOString(),
upload_time: new Date().toISOString()
}
},
{
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
}
}
);
logger.info('[DifyAdapter] Document uploaded', {
documentId: response.data.document.id
});
return {
success: true,
documentId: response.data.document.id
};
} catch (error: any) {
logger.error('[DifyAdapter] Upload failed', {
error: error.message,
name: metadata.name
});
return {
success: false
};
}
}
/**
* 获取项目对应的知识库ID
* @param projectId 项目ID
*/
private getKnowledgeBaseId(projectId: string): string {
// TODO: 从数据库读取项目配置
// 临时方案:从环境变量读取
return process.env.DIFY_KNOWLEDGE_BASE_ID || '';
}
}
/**
* Dify搜索结果
*/
export interface DifySearchResult {
success: boolean;
records: Array<{
content: string;
score: number;
metadata?: {
doc_type?: string;
date?: string;
};
}>;
query: string;
error?: string;
}
环境变量配置(.env):
# Dify配置
DIFY_API_URL=http://localhost/v1
DIFY_API_KEY=app-xxxxxxxxxxxxxxxxxxxxxx
DIFY_KNOWLEDGE_BASE_ID=kb-xxxxxxxxxxxxxxxxxxxxxx
验收标准:
- ✅ DifyAdapter类实现完整
- ✅ 可成功调用搜索API
- ✅ 可成功上传文档
- ✅ 错误处理完善
任务1.4:编写单元测试(2小时)
文件位置:backend/src/modules/iit-manager/adapters/__tests__/DifyAdapter.test.ts
import { DifyAdapter } from '../DifyAdapter';
describe('DifyAdapter', () => {
let difyAdapter: DifyAdapter;
beforeAll(() => {
difyAdapter = new DifyAdapter('test-project-id');
});
describe('searchKnowledge', () => {
it('应该成功搜索知识库', async () => {
const result = await difyAdapter.searchKnowledge('入组标准有哪些?');
expect(result.success).toBe(true);
expect(result.records.length).toBeGreaterThan(0);
expect(result.records[0]).toHaveProperty('content');
expect(result.records[0]).toHaveProperty('score');
});
it('应该支持按文档类型过滤', async () => {
const result = await difyAdapter.searchKnowledge(
'入组标准',
{ doc_type: 'protocol' }
);
expect(result.success).toBe(true);
expect(result.records.length).toBeGreaterThan(0);
});
it('应该处理搜索失败情况', async () => {
// Mock错误场景
const result = await difyAdapter.searchKnowledge('');
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
});
describe('uploadDocument', () => {
it('应该成功上传文档', async () => {
const result = await difyAdapter.uploadDocument(
'这是一份测试文档',
{
name: '测试文档',
doc_type: 'protocol'
}
);
expect(result.success).toBe(true);
expect(result.documentId).toBeDefined();
});
});
});
验收标准:
- ✅ 单元测试覆盖率>80%
- ✅ 所有测试用例通过
Day 2:意图识别与路由逻辑(8小时)
任务2.1:设计工具定义(Tool Schema)(2小时)
文件位置:backend/src/modules/iit-manager/agents/tools.ts
/**
* IIT Manager Agent工具定义
*/
export const iitAgentTools = [
// 工具1:查询实时数据
{
type: "function",
function: {
name: "query_clinical_data",
description: `【查REDCap实时数据】用于查询临床研究的实时数据状态。
适用场景:
- 问项目进度:现在入组多少人了?数据完整率如何?
- 问患者详情:P001患者录完数据了吗?有没有不良反应?
- 问质控状态:有哪些质控问题?数据质量怎么样?`,
parameters: {
type: "object",
properties: {
intent: {
type: "string",
enum: ["project_stats", "patient_detail", "qc_status"],
description: `查询意图:
- project_stats: 项目宏观统计(入组人数、数据完整率等)
- patient_detail: 特定患者详情(录入情况、不良反应等)
- qc_status: 质控状态(质疑列表、数据问题等)`
},
patient_id: {
type: "string",
description: "患者/受试者编号(如 P001、P002),当intent=patient_detail时必填"
},
date_range: {
type: "string",
enum: ["today", "this_week", "this_month", "all"],
description: "时间范围,默认为all"
}
},
required: ["intent"]
}
}
},
// 工具2:搜索知识库
{
type: "function",
function: {
name: "search_knowledge_base",
description: `【查研究文档】用于搜索研究方案、规范文件、历史记录等静态资料。
适用场景:
- 问研究规范:入排标准是什么?研究终点怎么定义?
- 问CRF表格:某个字段的定义是什么?填写规范是?
- 问历史记录:上周的周报里提到了什么问题?`,
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "搜索关键词或问题"
},
doc_category: {
type: "string",
enum: ["protocol", "crf", "report"],
description: `文档类别:
- protocol: 研究方案、伦理批件、知情同意书、入排标准
- crf: CRF表格定义、填写说明、数据字典
- report: 项目周报、进度总结、历史记录`
}
},
required: ["query"]
}
}
}
];
任务2.2:实现意图路由器(Intent Router)(3小时)
文件位置:backend/src/modules/iit-manager/agents/IntentRouter.ts
import OpenAI from 'openai';
import { logger } from '../../../common/logging/index.js';
import { iitAgentTools } from './tools.js';
/**
* 意图路由器
* 使用LLM的Function Calling能力识别用户意图
*/
export class IntentRouter {
private llm: OpenAI;
private systemPrompt: string;
constructor() {
this.llm = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.OPENAI_BASE_URL || 'https://api.deepseek.com'
});
this.systemPrompt = this.buildSystemPrompt();
}
/**
* 识别用户意图并返回工具调用
*/
async route(userMessage: string, context?: {
projectId: string;
userId: string;
}): Promise<IntentRouteResult> {
try {
logger.info('[IntentRouter] Routing user message', {
message: userMessage.substring(0, 100),
projectId: context?.projectId
});
const response = await this.llm.chat.completions.create({
model: 'deepseek-chat',
messages: [
{ role: 'system', content: this.systemPrompt },
{ role: 'user', content: userMessage }
],
tools: iitAgentTools,
tool_choice: 'auto',
temperature: 0.1, // 低温度,保证稳定性
max_tokens: 500
});
const message = response.choices[0].message;
// 如果LLM决定调用工具
if (message.tool_calls && message.tool_calls.length > 0) {
const toolCall = message.tool_calls[0];
const toolName = toolCall.function.name;
const toolArgs = JSON.parse(toolCall.function.arguments);
logger.info('[IntentRouter] Tool selected', {
toolName,
toolArgs
});
return {
needsToolCall: true,
toolName: toolName as 'query_clinical_data' | 'search_knowledge_base',
toolArgs,
rawResponse: message
};
}
// 如果LLM直接回答(不需要工具)
logger.info('[IntentRouter] Direct answer', {
answer: message.content?.substring(0, 100)
});
return {
needsToolCall: false,
directAnswer: message.content || '抱歉,我没有理解您的问题',
rawResponse: message
};
} catch (error: any) {
logger.error('[IntentRouter] Routing failed', {
error: error.message,
message: userMessage
});
return {
needsToolCall: false,
directAnswer: '抱歉,我遇到了一些问题,请稍后再试',
error: error.message
};
}
}
/**
* 构建System Prompt
*/
private buildSystemPrompt(): string {
return `# 角色
你是由壹证循科技开发的"临床研究项目助手",服务于IIT(研究者发起试验)项目的PI(主要研究者)。
# 能力
你拥有两个工具,请根据用户问题精准选择:
1. **query_clinical_data**(查实时数据)
- 当用户问"现状"时使用
- 例如:"现在入组多少人?"、"P001患者录完了吗?"、"有没有不良反应?"
- 这些问题需要查询REDCap数据库的实时数据
2. **search_knowledge_base**(查研究文档)
- 当用户问"规定"或"历史"时使用
- 例如:"入排标准是什么?"、"上周的问题解决了吗?"、"CRF里某字段怎么填?"
- 这些问题需要查阅研究方案、周报等文档
# 路由原则
- 如果问题明确需要工具,必须调用工具,不要猜测或编造答案
- 如果问题模糊,优先选择query_clinical_data(实时数据更重要)
- 如果是闲聊或打招呼,可以直接回答,不调用工具
# 约束
- 严禁编造数据
- 回答要简洁专业
- 隐去患者真实姓名,只使用编号`;
}
}
/**
* 意图路由结果
*/
export interface IntentRouteResult {
needsToolCall: boolean;
toolName?: 'query_clinical_data' | 'search_knowledge_base';
toolArgs?: any;
directAnswer?: string;
error?: string;
rawResponse?: any;
}
验收标准:
- ✅ IntentRouter类实现完整
- ✅ 可正确识别查数据意图
- ✅ 可正确识别查文档意图
- ✅ 可处理闲聊场景
任务2.3:实现工具执行器(Tool Executor)(3小时)
文件位置:backend/src/modules/iit-manager/agents/ToolExecutor.ts
import { DifyAdapter } from '../adapters/DifyAdapter.js';
import { RedcapAdapter } from '../adapters/RedcapAdapter.js';
import { logger } from '../../../common/logging/index.js';
import { prisma } from '../../../config/database.js';
/**
* 工具执行器
* 根据意图路由结果执行对应的工具
*/
export class ToolExecutor {
/**
* 执行工具
*/
async execute(
toolName: 'query_clinical_data' | 'search_knowledge_base',
toolArgs: any,
context: {
projectId: string;
userId: string;
}
): Promise<ToolExecutionResult> {
try {
logger.info('[ToolExecutor] Executing tool', {
toolName,
toolArgs,
projectId: context.projectId
});
if (toolName === 'query_clinical_data') {
return await this.executeQueryClinicalData(toolArgs, context);
} else if (toolName === 'search_knowledge_base') {
return await this.executeSearchKnowledge(toolArgs, context);
}
return {
success: false,
data: null,
error: `Unknown tool: ${toolName}`
};
} catch (error: any) {
logger.error('[ToolExecutor] Execution failed', {
error: error.message,
toolName
});
return {
success: false,
data: null,
error: error.message
};
}
}
/**
* 执行:查询临床数据
*/
private async executeQueryClinicalData(
args: {
intent: 'project_stats' | 'patient_detail' | 'qc_status';
patient_id?: string;
date_range?: string;
},
context: { projectId: string; userId: string }
): Promise<ToolExecutionResult> {
// 1. 获取项目配置
const project = await prisma.iitProject.findUnique({
where: { id: context.projectId },
select: {
redcapApiUrl: true,
redcapApiToken: true,
redcapProjectId: true
}
});
if (!project) {
return {
success: false,
data: null,
error: '项目不存在'
};
}
// 2. 初始化RedcapAdapter
const redcap = new RedcapAdapter(
project.redcapApiUrl,
project.redcapApiToken
);
// 3. 根据intent执行不同查询
switch (args.intent) {
case 'project_stats': {
// 查询项目统计
const records = await redcap.exportRecords();
return {
success: true,
data: {
type: 'project_stats',
totalRecords: records.length,
stats: {
enrolled: records.length,
completed: records.filter((r: any) => r.complete === '2').length,
dataQuality: '87.5%' // TODO: 实际计算
}
}
};
}
case 'patient_detail': {
// 查询特定患者
if (!args.patient_id) {
return {
success: false,
data: null,
error: '缺少患者ID'
};
}
const records = await redcap.exportRecords([args.patient_id]);
if (records.length === 0) {
return {
success: false,
data: null,
error: `未找到患者 ${args.patient_id}`
};
}
return {
success: true,
data: {
type: 'patient_detail',
patientId: args.patient_id,
details: records[0]
}
};
}
case 'qc_status': {
// 查询质控状态
const logs = await prisma.iitAuditLog.findMany({
where: {
projectId: context.projectId,
actionType: 'quality_issue'
},
orderBy: { createdAt: 'desc' },
take: 10
});
return {
success: true,
data: {
type: 'qc_status',
issueCount: logs.length,
recentIssues: logs.map(log => ({
recordId: log.entityId,
issue: log.details,
createdAt: log.createdAt
}))
}
};
}
default:
return {
success: false,
data: null,
error: `Unknown intent: ${args.intent}`
};
}
}
/**
* 执行:搜索知识库
*/
private async executeSearchKnowledge(
args: {
query: string;
doc_category?: 'protocol' | 'crf' | 'report';
},
context: { projectId: string; userId: string }
): Promise<ToolExecutionResult> {
// 1. 初始化DifyAdapter
const dify = new DifyAdapter(context.projectId);
// 2. 搜索知识库
const result = await dify.searchKnowledge(args.query, {
doc_type: args.doc_category,
top_k: 3
});
if (!result.success) {
return {
success: false,
data: null,
error: result.error || '知识库搜索失败'
};
}
// 3. 格式化结果
return {
success: true,
data: {
type: 'knowledge_search',
query: args.query,
category: args.doc_category,
results: result.records.map(record => ({
content: record.content,
score: record.score,
metadata: record.metadata
}))
}
};
}
}
/**
* 工具执行结果
*/
export interface ToolExecutionResult {
success: boolean;
data: any;
error?: string;
}
验收标准:
- ✅ ToolExecutor类实现完整
- ✅ query_clinical_data工具可执行
- ✅ search_knowledge_base工具可执行
- ✅ 错误处理完善
Day 3:集成企业微信对话(8小时)
任务3.1:增强WechatCallbackController(3小时)
修改文件:backend/src/modules/iit-manager/controllers/WechatCallbackController.ts
在现有的handleCallback方法中增加AI对话逻辑:
// 在WechatCallbackController类中添加
import { IntentRouter } from '../agents/IntentRouter.js';
import { ToolExecutor } from '../agents/ToolExecutor.js';
import { AnswerGenerator } from '../agents/AnswerGenerator.js';
class WechatCallbackController {
private intentRouter: IntentRouter;
private toolExecutor: ToolExecutor;
private answerGenerator: AnswerGenerator;
constructor() {
// ... 现有代码 ...
this.intentRouter = new IntentRouter();
this.toolExecutor = new ToolExecutor();
this.answerGenerator = new AnswerGenerator();
}
/**
* 处理企业微信回调消息(已有方法,增强)
*/
async handleCallback(request: FastifyRequest, reply: FastifyReply): Promise<void> {
// ... 现有的验证、解密逻辑 ...
// ✅ 立即返回success(避免5秒超时)
reply.send('success');
// ✅ 异步处理用户消息(新增)
setImmediate(async () => {
try {
const userMessage = decryptedData.Content;
const userId = decryptedData.FromUserName;
logger.info('📥 收到用户消息', {
userId,
message: userMessage.substring(0, 50)
});
// 1. 获取用户的项目信息
const userMapping = await prisma.iitUserMapping.findFirst({
where: { wechatUserId: userId }
});
if (!userMapping) {
await wechatService.sendTextMessage(
userId,
'⚠️ 您还未绑定项目,请联系管理员配置'
);
return;
}
// 2. 意图识别
const routeResult = await this.intentRouter.route(userMessage, {
projectId: userMapping.projectId,
userId
});
// 3. 如果直接回答(不需要工具)
if (!routeResult.needsToolCall) {
await wechatService.sendTextMessage(userId, routeResult.directAnswer!);
return;
}
// 4. 执行工具
const toolResult = await this.toolExecutor.execute(
routeResult.toolName!,
routeResult.toolArgs,
{
projectId: userMapping.projectId,
userId
}
);
// 5. 生成回答
const answer = await this.answerGenerator.generate(
userMessage,
toolResult,
routeResult.toolName!
);
// 6. 发送回复
await wechatService.sendMarkdownMessage(userId, answer);
// 7. 记录审计日志
await prisma.iitAuditLog.create({
data: {
projectId: userMapping.projectId,
actionType: 'wechat_user_query',
operator: userId,
entityId: userId,
details: {
question: userMessage,
answer: answer.substring(0, 200),
toolUsed: routeResult.toolName
}
}
});
logger.info('✅ 回答发送成功', {
userId,
toolUsed: routeResult.toolName
});
} catch (error: any) {
logger.error('❌ 处理用户消息失败', {
error: error.message
});
// 发送错误提示
await wechatService.sendTextMessage(
userId,
'抱歉,我遇到了一些问题,请稍后再试或联系管理员'
);
}
});
}
}
验收标准:
- ✅ 可接收用户消息
- ✅ 可调用意图路由
- ✅ 可执行工具
- ✅ 可生成回答
- ✅ 可发送回复
任务3.2:实现答案生成器(Answer Generator)(2小时)
文件位置:backend/src/modules/iit-manager/agents/AnswerGenerator.ts
import OpenAI from 'openai';
import { logger } from '../../../common/logging/index.js';
import { ToolExecutionResult } from './ToolExecutor.js';
/**
* 答案生成器
* 将工具执行结果转换为用户友好的回答
*/
export class AnswerGenerator {
private llm: OpenAI;
constructor() {
this.llm = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.OPENAI_BASE_URL || 'https://api.deepseek.com'
});
}
/**
* 生成回答
*/
async generate(
userQuestion: string,
toolResult: ToolExecutionResult,
toolName: string
): Promise<string> {
try {
logger.info('[AnswerGenerator] Generating answer', {
question: userQuestion.substring(0, 50),
toolName,
success: toolResult.success
});
// 如果工具执行失败
if (!toolResult.success) {
return this.generateErrorMessage(toolResult.error);
}
// 根据不同工具类型,使用不同的回答模板
if (toolName === 'query_clinical_data') {
return this.generateDataAnswer(userQuestion, toolResult.data);
} else if (toolName === 'search_knowledge_base') {
return await this.generateKnowledgeAnswer(userQuestion, toolResult.data);
}
return '抱歉,我无法生成回答';
} catch (error: any) {
logger.error('[AnswerGenerator] Generation failed', {
error: error.message
});
return '抱歉,回答生成失败,请稍后再试';
}
}
/**
* 生成数据查询的回答(使用模板,不调用LLM)
*/
private generateDataAnswer(question: string, data: any): string {
const type = data.type;
if (type === 'project_stats') {
return `📊 **项目统计数据**
✅ **入组人数**:${data.stats.enrolled}例
✅ **完成病例**:${data.stats.completed}例
✅ **数据质量**:${data.stats.dataQuality}
💡 数据更新时间:${new Date().toLocaleString('zh-CN')}`;
}
if (type === 'patient_detail') {
const details = data.details;
return `👤 **患者 ${data.patientId} 详情**
📋 **基本信息**:
- 年龄:${details.age || '未录入'}岁
- 性别:${details.gender || '未录入'}
- BMI:${details.bmi || '未录入'}
📊 **录入状态**:
- 数据完整度:${details.complete === '2' ? '✅ 已完成' : '⏳ 进行中'}
💡 最后更新:${new Date().toLocaleString('zh-CN')}`;
}
if (type === 'qc_status') {
const issues = data.recentIssues.slice(0, 5);
let answer = `🔍 **质控状态**\n\n`;
answer += `⚠️ **质控问题数**:${data.issueCount}个\n\n`;
if (issues.length > 0) {
answer += `📋 **最近问题**:\n`;
issues.forEach((issue: any, index: number) => {
answer += `${index + 1}. 记录${issue.recordId}:${JSON.stringify(issue.issue).substring(0, 50)}\n`;
});
} else {
answer += `✅ 暂无质控问题`;
}
return answer;
}
return JSON.stringify(data, null, 2);
}
/**
* 生成知识检索的回答(调用LLM综合)
*/
private async generateKnowledgeAnswer(question: string, data: any): Promise<string> {
const results = data.results;
if (results.length === 0) {
return `📚 **知识库搜索**
❌ 未找到相关内容
💡 建议:
1. 尝试换个关键词
2. 查看研究方案原文
3. 联系项目协调员`;
}
// 将搜索结果拼接为上下文
const context = results
.map((r: any, index: number) => `[文档${index + 1}] ${r.content}`)
.join('\n\n');
// 调用LLM综合回答
const response = await this.llm.chat.completions.create({
model: 'deepseek-chat',
messages: [
{
role: 'system',
content: `你是临床研究助手。根据检索到的文档内容,回答用户问题。
要求:
1. 回答要准确、简洁
2. 引用文档时标注[文档X]
3. 如果文档中没有明确答案,诚实说明
4. 使用Markdown格式`
},
{
role: 'user',
content: `用户问题:${question}\n\n检索到的文档内容:\n${context}`
}
],
temperature: 0.3,
max_tokens: 800
});
const answer = response.choices[0].message.content || '无法生成回答';
return `📚 **知识库查询结果**\n\n${answer}`;
}
/**
* 生成错误提示
*/
private generateErrorMessage(error?: string): string {
return `❌ 查询失败
原因:${error || '未知错误'}
💡 您可以:
1. 稍后重试
2. 换个问法
3. 联系管理员`;
}
}
验收标准:
- ✅ 可生成数据查询回答
- ✅ 可生成知识检索回答
- ✅ 回答格式友好(Markdown)
- ✅ 错误提示清晰
任务3.3:端到端测试(3小时)
测试场景:
// 测试场景1:查询项目统计
{
input: "现在入组多少人了?",
expectedTool: "query_clinical_data",
expectedIntent: "project_stats",
expectedOutput: "📊 项目统计数据\n✅ 入组人数:XX例"
}
// 测试场景2:查询特定患者
{
input: "P001患者录完数据了吗?",
expectedTool: "query_clinical_data",
expectedIntent: "patient_detail",
expectedOutput: "👤 患者 P001 详情"
}
// 测试场景3:查询研究方案
{
input: "入排标准是什么?",
expectedTool: "search_knowledge_base",
expectedCategory: "protocol",
expectedOutput: "📚 知识库查询结果"
}
// 测试场景4:查询CRF表格
{
input: "BMI这个字段怎么填?",
expectedTool: "search_knowledge_base",
expectedCategory: "crf",
expectedOutput: "📚 知识库查询结果"
}
// 测试场景5:闲聊
{
input: "你好",
expectedTool: null,
expectedOutput: "您好!我是临床研究助手"
}
测试步骤:
- 在企业微信中发送测试消息
- 观察后端日志
- 验证回复内容
- 检查审计日志
验收标准:
- ✅ 5个测试场景全部通过
- ✅ 回复时间<3秒
- ✅ 回复内容准确
- ✅ 审计日志完整
Day 4:周报自动归档(6小时)
任务4.1:实现周报生成器(3小时)
文件位置:backend/src/modules/iit-manager/services/WeeklyReportGenerator.ts
import { prisma } from '../../../config/database.js';
import { RedcapAdapter } from '../adapters/RedcapAdapter.js';
import { DifyAdapter } from '../adapters/DifyAdapter.js';
import { logger } from '../../../common/logging/index.js';
import { getISOWeek, startOfWeek, endOfWeek } from 'date-fns';
/**
* 周报生成器
* 自动生成项目周报并上传到Dify知识库
*/
export class WeeklyReportGenerator {
/**
* 生成并上传周报
*/
async generateAndUpload(projectId: string): Promise<{
success: boolean;
reportId?: string;
error?: string;
}> {
try {
const weekNumber = getISOWeek(new Date());
const year = new Date().getFullYear();
logger.info('[WeeklyReportGenerator] Starting generation', {
projectId,
year,
weekNumber
});
// 1. 检查是否已生成
const existing = await prisma.iitWeeklyReport.findFirst({
where: {
projectId,
year,
weekNumber
}
});
if (existing) {
logger.warn('[WeeklyReportGenerator] Report already exists', {
reportId: existing.id
});
return {
success: false,
error: '本周周报已生成'
};
}
// 2. 收集数据
const reportData = await this.collectWeeklyData(projectId, year, weekNumber);
// 3. 生成Markdown内容
const content = this.generateMarkdownContent(reportData, year, weekNumber);
// 4. 保存到数据库
const report = await prisma.iitWeeklyReport.create({
data: {
projectId,
year,
weekNumber,
content,
stats: reportData.stats,
createdAt: new Date()
}
});
// 5. 上传到Dify知识库
const dify = new DifyAdapter(projectId);
const uploadResult = await dify.uploadDocument(content, {
name: `周报-${year}年第${weekNumber}周`,
doc_type: 'report',
date: `${year}-W${weekNumber.toString().padStart(2, '0')}`
});
if (!uploadResult.success) {
logger.error('[WeeklyReportGenerator] Dify upload failed');
// 数据库已保存,Dify上传失败不影响
}
logger.info('[WeeklyReportGenerator] Generation completed', {
reportId: report.id,
difyUploaded: uploadResult.success
});
return {
success: true,
reportId: report.id
};
} catch (error: any) {
logger.error('[WeeklyReportGenerator] Generation failed', {
error: error.message,
projectId
});
return {
success: false,
error: error.message
};
}
}
/**
* 收集本周数据
*/
private async collectWeeklyData(
projectId: string,
year: number,
weekNumber: number
): Promise<any> {
const weekStart = startOfWeek(new Date(), { weekStartsOn: 1 });
const weekEnd = endOfWeek(new Date(), { weekStartsOn: 1 });
// 1. 获取项目配置
const project = await prisma.iitProject.findUnique({
where: { id: projectId },
select: {
name: true,
redcapApiUrl: true,
redcapApiToken: true
}
});
// 2. 从REDCap获取统计
const redcap = new RedcapAdapter(
project!.redcapApiUrl,
project!.redcapApiToken
);
const allRecords = await redcap.exportRecords();
// 3. 从审计日志获取本周活动
const weeklyLogs = await prisma.iitAuditLog.findMany({
where: {
projectId,
createdAt: {
gte: weekStart,
lte: weekEnd
}
},
orderBy: { createdAt: 'desc' }
});
// 4. 统计数据
const stats = {
totalRecords: allRecords.length,
newRecordsThisWeek: weeklyLogs.filter(
log => log.actionType === 'redcap_data_received'
).length,
qualityIssues: weeklyLogs.filter(
log => log.actionType === 'quality_issue'
).length,
wechatNotifications: weeklyLogs.filter(
log => log.actionType === 'wechat_notification_sent'
).length
};
return {
projectName: project!.name,
year,
weekNumber,
weekStart: weekStart.toISOString(),
weekEnd: weekEnd.toISOString(),
stats,
recentActivities: weeklyLogs.slice(0, 20).map(log => ({
actionType: log.actionType,
entityId: log.entityId,
createdAt: log.createdAt,
details: log.details
}))
};
}
/**
* 生成Markdown内容
*/
private generateMarkdownContent(data: any, year: number, weekNumber: number): string {
return `# ${data.projectName} - ${year}年第${weekNumber}周周报
## 📊 统计数据
- **总记录数**:${data.stats.totalRecords}例
- **本周新增**:${data.stats.newRecordsThisWeek}例
- **质控问题**:${data.stats.qualityIssues}个
- **企业微信通知**:${data.stats.wechatNotifications}次
## 📅 时间范围
- **开始时间**:${new Date(data.weekStart).toLocaleString('zh-CN')}
- **结束时间**:${new Date(data.weekEnd).toLocaleString('zh-CN')}
## 📋 本周主要活动
${data.recentActivities
.map((activity: any, index: number) => {
return `${index + 1}. **${activity.actionType}** - 记录${activity.entityId} (${new Date(activity.createdAt).toLocaleString('zh-CN')})`;
})
.join('\n')}
## 💡 重点关注
${data.stats.qualityIssues > 0
? `⚠️ 本周发现${data.stats.qualityIssues}个质控问题,请及时处理`
: '✅ 本周无质控问题'}
---
*自动生成时间:${new Date().toLocaleString('zh-CN')}*`;
}
}
// 数据库表定义(需要添加到Prisma Schema)
/*
model IitWeeklyReport {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
projectId String @db.Uuid
year Int
weekNumber Int
content String @db.Text
stats Json
createdAt DateTime @default(now())
project IitProject @relation(fields: [projectId], references: [id])
@@unique([projectId, year, weekNumber])
@@map("weekly_reports")
@@schema("iit_schema")
}
*/
验收标准:
- ✅ 可生成周报Markdown
- ✅ 可保存到数据库
- ✅ 可上传到Dify
- ✅ 防止重复生成
任务4.2:配置定时任务(2小时)
文件位置:backend/src/modules/iit-manager/index.ts
import cron from 'node-cron';
import { WeeklyReportGenerator } from './services/WeeklyReportGenerator.js';
/**
* 初始化IIT Manager模块
*/
export async function initIitManager() {
// ... 现有的Worker注册代码 ...
// ✅ 新增:注册周报定时任务
registerWeeklyReportCron();
logger.info('✅ IIT Manager initialized');
}
/**
* 注册周报定时任务
* 每周一 00:00 自动生成上周周报
*/
function registerWeeklyReportCron() {
const generator = new WeeklyReportGenerator();
// 每周一凌晨0点执行
cron.schedule('0 0 * * 1', async () => {
logger.info('⏰ 开始生成周报');
try {
// 获取所有活跃项目
const projects = await prisma.iitProject.findMany({
where: { status: 'active' }
});
for (const project of projects) {
await generator.generateAndUpload(project.id);
}
logger.info('✅ 周报生成完成', {
projectCount: projects.length
});
} catch (error: any) {
logger.error('❌ 周报生成失败', {
error: error.message
});
}
}, {
timezone: 'Asia/Shanghai'
});
logger.info('✅ 周报定时任务已注册(每周一 00:00)');
}
验收标准:
- ✅ 定时任务注册成功
- ✅ 可手动触发测试
- ✅ 日志记录完整
Day 5:文档编写与测试(6小时)
任务5.1:用户使用手册(2小时)
文件位置:AIclinicalresearch/docs/03-业务模块/IIT Manager Agent/05-使用手册/企业微信对话指南.md
内容大纲:
- 功能介绍
- 支持的查询类型
- 常用问法示例
- 注意事项
- 常见问题FAQ
任务5.2:Phase 1.5开发记录(2小时)
文件位置:AIclinicalresearch/docs/03-业务模块/IIT Manager Agent/06-开发记录/Phase1.5-AI对话能力开发完成记录.md
内容大纲:
- 开发目标与成果
- 技术实现细节
- 测试验证结果
- 已知限制与改进计划
任务5.3:完整测试(2小时)
测试矩阵:
| 场景 | 输入 | 预期工具 | 预期输出 | 状态 |
|---|---|---|---|---|
| 项目统计 | "现在入组多少人?" | query_clinical_data | 包含入组人数 | ⏳ |
| 患者详情 | "P001录完了吗?" | query_clinical_data | 包含患者状态 | ⏳ |
| 质控状态 | "有哪些质控问题?" | query_clinical_data | 问题列表 | ⏳ |
| 研究方案 | "入排标准是什么?" | search_knowledge_base | 方案内容 | ⏳ |
| CRF查询 | "BMI怎么填?" | search_knowledge_base | CRF说明 | ⏳ |
| 周报查询 | "上周进展如何?" | search_knowledge_base | 周报内容 | ⏳ |
| 闲聊 | "你好" | 无 | 友好回复 | ⏳ |
📊 四、成功标准与验收
4.1 功能完整性
| 功能 | 验收标准 | 优先级 |
|---|---|---|
| Dify集成 | 可成功调用本地Dify API | 🔴 P0 |
| 意图识别 | 准确率>80% | 🔴 P0 |
| 数据查询 | 可查询REDCap实时数据 | 🔴 P0 |
| 知识检索 | 可检索研究方案文档 | 🔴 P0 |
| 企业微信回复 | 回复时间<3秒 | 🔴 P0 |
| 周报自动归档 | 每周一自动生成 | 🟠 P1 |
| 审计日志 | 所有对话有日志 | 🟠 P1 |
4.2 性能指标
| 指标 | 目标 | 说明 |
|---|---|---|
| 回复延迟 | <3秒 | 用户问 → 收到回复 |
| Dify查询延迟 | <500ms | 本地部署,应该很快 |
| REDCap查询延迟 | <1秒 | 已有adapter,已验证 |
| 意图识别准确率 | >80% | 通过测试矩阵验证 |
| 知识检索准确率 | >70% | 依赖文档质量 |
4.3 代码质量
- ✅ TypeScript类型完整
- ✅ 单元测试覆盖率>70%
- ✅ 集成测试通过
- ✅ 错误处理完善
- ✅ 日志记录完整
- ✅ 代码符合规范
🚀 五、部署与上线
5.1 环境变量配置
# Dify配置
DIFY_API_URL=http://localhost/v1
DIFY_API_KEY=app-xxxxxxxxxxxxxxxxxxxxxx
DIFY_KNOWLEDGE_BASE_ID=kb-xxxxxxxxxxxxxxxxxxxxxx
# LLM配置(DeepSeek)
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxx
OPENAI_BASE_URL=https://api.deepseek.com
# 企业微信配置(已有)
WECHAT_CORP_ID=ww01cb7b72ea2db83c
WECHAT_CORP_SECRET=xxx
WECHAT_AGENT_ID=1000002
5.2 数据库迁移
# 添加周报表
npx prisma db push
npx prisma generate
5.3 重启服务
cd AIclinicalresearch/backend
npm run dev
📚 六、技术债务与改进计划
6.1 当前限制(Phase 1.5)
| 限制 | 影响 | 计划改进时间 |
|---|---|---|
| 单项目支持 | 只能服务一个项目 | Phase 2 |
| 无多轮对话 | 每次问答独立 | Phase 2 |
| 无上下文记忆 | 不记得之前的对话 | Phase 2 |
| 硬编码知识库ID | 不支持多项目 | Phase 2 |
| 简单意图识别 | 不支持复杂推理 | Phase 3 |
6.2 Phase 2 改进计划
-
多项目支持
- 每个项目独立知识库
- 用户权限管理
- 项目切换功能
-
多轮对话
- 会话状态管理
- 上下文记忆(Redis)
- 澄清式提问
-
混合推理(ReAct)
- 支持复杂查询
- 多工具组合
- 自主推理循环
✅ 七、总结
7.1 Phase 1.5核心价值
实现目标:
- ✅ PI可在企业微信中自然语言提问
- ✅ AI可理解意图并查询数据/文档
- ✅ 回答准确、及时、友好
- ✅ 周报自动归档,可随时查询
技术亮点:
- 🎯 基于本地Dify,无API成本
- 🧠 单步意图识别,简单高效
- 🔧 复用现有RedcapAdapter
- 📊 周报自动生成与归档
- 🚀 端到端延迟<3秒
开发效率:
- 📅 预估工作量:5天
- 📝 新增代码:~2000行
- 🧪 测试覆盖:>70%
- 📚 文档完整:用户手册+开发记录
下一步:Phase 2 - 多项目支持与高级对话能力
🎯 八、极简版核心价值总结
8.1 为什么极简版最重要?
用户反馈:
"最重要的是先让AI能对话,其他都先放一边"
现实情况:
- ✅ MVP闭环已打通(Day 3完成)
- ✅ 企业微信推送已验证(100%成功率)
- ✅ RedcapAdapter已可用(直接复用)
- ⚠️ 缺少:PI无法主动查询数据
极简版价值:
- 🚀 2天上线:最快实现AI对话
- 💰 零成本:只查REDCap,不用Dify
- 🧠 有记忆:支持多轮对话(3轮)
- ⚡ 有反馈:"正在查询..."避免用户焦虑
- 🎯 核心够用:满足80%的查询需求
8.2 三大核心改进(基于用户建议)
改进1:上下文记忆 ✅
问题:
PI: "帮我查一下P001的入组情况"
AI: "P001已入组"
PI: "他有不良反应吗?"
AI: ❌ "请提供患者编号"(失忆了)
解决:
// SessionMemory:存储最近3轮对话
sessionMemory.addMessage(userId, 'user', '帮我查P001');
sessionMemory.addMessage(userId, 'assistant', 'P001已入组');
// 下次查询时,自动填充患者ID
const lastPatientId = sessionMemory.getLastPatientId(userId); // P001
效果:
PI: "他有不良反应吗?"
AI: ✅ "查询P001:无不良反应记录"(记得是P001)
改进2:正在输入反馈 ✅
问题:
- AI处理需要5-8秒
- 用户发完消息后,手机没反应
- 用户以为系统挂了
解决:
// 立即发送临时反馈
await wechatService.sendTextMessage(userId, '🫡 正在查询,请稍候...');
// 再慢慢处理
const answer = await processQuery(userMessage);
await wechatService.sendMarkdownMessage(userId, answer);
效果:
- ✅ 用户立即看到反馈(<1秒)
- ✅ 知道AI在工作
- ✅ 不会焦虑
改进3:极简优先 ✅
问题:
- 原计划5天开发太长
- Dify、周报等功能非必需
- 用户最想要:能对话
解决:
- ✅ 只做REDCap查询(复用现有adapter)
- ✅ 不接Dify(Phase 2再做)
- ✅ 不做周报(Phase 2再做)
- ✅ 2天上线
效果:
- ✅ 快速验证价值
- ✅ 快速收集反馈
- ✅ 快速迭代
8.3 前端架构演进路线
Phase 1.5(当前):
├── 企业微信原生对话(省事)
├── 无自定义UI
└── 上下文存在Node.js内存
↓ (用户反馈 + 需求增长)
Phase 3(未来):
├── 自研H5/小程序(Taro 4.x)
├── Ant Design X管理上下文
├── 丰富的UI组件(输入提示、历史记录、知识卡片)
└── 更好的用户体验
为什么分两阶段?
- ✅ Phase 1.5:验证核心价值(AI能回答问题)
- ✅ Phase 3:优化用户体验(更美观、更智能)
- ✅ 避免过度设计(先有再好)
8.4 立即行动指南
Step 1:创建第一个文件(5分钟)
cd AIclinicalresearch/backend/src/modules/iit-manager
mkdir -p agents
touch agents/SessionMemory.ts
复制Day 1任务1.1的代码到 SessionMemory.ts
Step 2:运行单元测试(可选)
npm test agents/SessionMemory.test.ts
Step 3:继续Day 1其他任务
按照文档中的顺序:
- ✅ SessionMemory (30分钟)
- ⏳ SimpleIntentRouter (2小时)
- ⏳ SimpleToolExecutor (1.5小时)
- ⏳ SimpleAnswerGenerator (1小时)
- ⏳ 集成到WechatCallbackController (1小时)
Step 4:Day 1结束时测试
在企业微信中发送:
"现在入组多少人?"
预期:
- 立即收到"🫡 正在查询,请稍候..."
- 3秒内收到"📊 项目统计..."
Step 5:Day 2测试多轮对话
PI: "帮我查一下P001的情况"
AI: "👤 患者 P001 详情..."
PI: "他有不良反应吗?" ← 测试上下文
AI: "查询P001:无不良反应记录" ← 应该自动识别
8.5 成功标准(极简版)
| 检查项 | 标准 | 验证方式 |
|---|---|---|
| ✅ 基础对话 | 能回答"入组多少人" | 企微测试 |
| ✅ 患者查询 | 能回答"P001情况" | 企微测试 |
| ✅ 上下文记忆 | "他有不良反应吗"能识别P001 | 企微测试 |
| ✅ 正在输入反馈 | <1秒收到"正在查询..." | 企微测试 |
| ✅ 最终回复 | <3秒收到完整答案 | 后端日志 |
| ✅ 审计日志 | 记录上下文标记 | 数据库检查 |
8.6 与完整版的关系
极简版(2天):
- 🎯 目标:最快验证AI对话价值
- 📦 范围:REDCap查询 + 上下文记忆
- 💰 成本:无额外成本(复用现有)
- 🚀 速度:2天上线
完整版(5天):
- 🎯 目标:全面的AI助手能力
- 📦 范围:+ Dify知识库 + 周报归档 + 文档查询
- 💰 成本:需配置Dify(已有Docker)
- 🚀 速度:5天上线
建议:
✅ 先做极简版(2天),验证价值
✅ 收集用户反馈
✅ 再决定是否做完整版
实际执行:
✅ 极简版已完成(2026-01-03)
✅ Dify知识库已集成(2026-01-04)
✅ 混合检索已实现:REDCap实时数据 + Dify文档知识库
🎓 八、Dify知识库集成(2026-01-04完成)
8.7 集成背景
完成时间: 2026-01-04
开发工作量: 4-6小时
集成目标: 在REDCap实时数据查询基础上,增加研究方案文档查询能力
核心价值:
- 📚 文档查询: 查询研究方案、CRF表格、伦理文件
- 🔀 混合检索: 同时支持结构化数据(REDCap)和非结构化文档(Dify)
- 🎯 智能路由: 根据用户问题自动选择数据源
8.8 技术方案
方案选择
| 维度 | 采用方案 |
|---|---|
| 知识库架构 | 单项目单知识库(1个IIT项目 → 1个Dify Dataset) |
| 文档上传 | Dify Web界面手动上传(MVP阶段) |
| 项目关联 | 用户绑定默认项目(存储在iit_schema.projects.dify_dataset_id) |
核心实现
1. 扩展意图识别
在ChatService.detectIntent()中新增query_protocol意图:
// 识别文档查询(研究方案、伦理、知情同意、CRF等)
if (/(研究方案|伦理|知情同意|CRF|病例报告表|纳入|入选|排除|标准|入组标准|治疗方案|试验设计|研究目的|研究流程|观察指标|诊断标准|疾病标准)/.test(message)) {
return { intent: 'query_protocol' };
}
2. 新增Dify查询方法
private async queryDifyKnowledge(query: string): Promise<string> {
// 1. 获取项目的difyDatasetId
const project = await prisma.iitProject.findFirst({
where: { status: 'active' },
select: { name: true, difyDatasetId: true }
});
// 2. 调用Dify API检索
const retrievalResult = await difyClient.retrieveKnowledge(
project.difyDatasetId,
query,
{ retrieval_model: { search_method: 'semantic_search', top_k: 5 } }
);
// 3. 格式化检索结果
// 修复bug:使用正确的字段路径 record.segment.document.name 和 record.segment.content
// ...
}
3. 更新对话流程
async handleMessage(userId: string, userMessage: string): Promise<string> {
const { intent, params } = this.detectIntent(userMessage);
// REDCap查询
let toolResult: any = null;
if (intent === 'query_record') {
toolResult = await this.queryRedcapRecord(params.recordId);
}
// Dify知识库查询
let difyKnowledge: string = '';
if (intent === 'query_protocol') {
difyKnowledge = await this.queryDifyKnowledge(userMessage);
}
// 构建LLM消息(同时注入REDCap数据和Dify知识)
const messages = this.buildMessagesWithData(
userMessage, context, toolResult, difyKnowledge, userId
);
// 调用LLM生成回答
const response = await this.llm.chat(messages);
// ...
}
8.9 问题排查与修复
问题1: AI不查询Dify,自己编造答案
现象: 用户问"纳入标准是什么?",AI编造了答案,Dify控制台无查询记录
根因1: 意图识别关键词不全
- 缺少: "入选"、"诊断标准"、"疾病标准"
- 解决: 扩充关键词列表
根因2: Dify API返回字段路径错误
- 错误:
record.document_name、record.content→ 返回undefined - 正确:
record.segment.document.name、record.segment.content - 解决: 修正字段访问路径
调试过程:
- 创建
debug-dify-injection.ts追踪数据注入流程 - 创建
inspect-dify-response.ts查看Dify API实际返回结构 - 发现并修复字段路径错误
8.10 测试验证
| 测试场景 | 问题 | 数据源 | 结果 |
|---|---|---|---|
| 文档查询 | "这个研究的排除标准是什么?" | Dify | ✅ 成功 |
| CRF查询 | "CRF表格中有哪些观察指标?" | Dify | ✅ 成功 |
| 患者查询 | "ID 7的患者情况" | REDCap | ✅ 成功 |
| 统计查询 | "目前入组了多少人?" | REDCap | ✅ 成功 |
| 混合查询 | "这个研究的主要研究目的是什么?" | Dify | ✅ 成功 |
8.11 集成成果
技术架构:
用户提问 → 意图识别 → ┬→ [query_protocol] → Dify API → 文档片段
├→ [query_record] → REDCap API → 患者数据
└→ [count_records] → REDCap API → 统计数据
↓
构建LLM Prompt(System + Data + Context)
↓
DeepSeek-V3
↓
AI回答
核心能力:
- ✅ 混合检索: 同时支持结构化数据和非结构化文档
- ✅ 智能路由: 根据意图自动选择数据源
- ✅ 防止幻觉: 所有回答基于真实数据/文档
- ✅ 来源标注: 清晰标注数据来自REDCap或Dify
详细记录: 参见 Dify知识库集成开发记录
✅ 九、总结
核心成就(极简版 + Dify集成)
- ✅ 2天上线:最快实现AI对话能力(含Dify集成)
- ✅ 上下文记忆:支持多轮对话(3轮)
- ✅ 正在输入反馈:避免用户焦虑
- ✅ 代词解析:"他"能自动识别患者
- ✅ 混合检索:同时支持REDCap实时数据 + Dify文档知识库
- ✅ 防止幻觉:所有回答基于真实数据,绝不编造
技术亮点
- 🧠 SessionMemory:内存存储,无需Redis
- 🎯 单步路由:不用复杂ReAct循环
- 🔧 复用现有:RedcapAdapter + WechatService
- ⚡ 性能保证:<3秒端到端延迟
- 📊 审计完整:记录所有对话
用户价值
Before(Day 3):
- ✅ PI可以接收企业微信通知
- ❌ PI无法主动查询数据
- ❌ 需要登录REDCap查看
After(Phase 1.5 + Dify集成):
- ✅ PI可以在企业微信中直接问"入组多少人"(REDCap)
- ✅ PI可以问"P001有不良反应吗"(REDCap)
- ✅ PI可以问"研究的纳入排除标准是什么"(Dify)
- ✅ PI可以问"CRF表格中有哪些观察指标"(Dify)
- ✅ AI记得上一轮对话,支持代词
- ✅ 回复快速(<6秒),有反馈
- ✅ AI基于真实数据/文档回答,不编造
🎉 Phase 1.5 开发完成总结 (2026-01-03 & 2026-01-04)
实际完成情况
- ✅ Day 1完成 (2026-01-03): SessionMemory + ChatService + REDCap集成
- ✅ Day 2完成 (2026-01-04): Dify知识库集成 + 混合检索
- ✅ 测试通过: 企业微信对话 + 真实数据查询 + 文档查询
- ✅ 核心突破: 解决LLM幻觉问题 + 混合检索架构
关键成果
- ✅ AI基于REDCap真实数据回答,不编造
- ✅ AI基于Dify知识库文档回答研究方案问题
- ✅ 混合检索:同时支持结构化数据和非结构化文档
- ✅ 从数据库读取项目配置(test0102)
- ✅ 意图识别 + 智能路由 + 数据查询 + LLM集成
- ✅ 上下文记忆(最近3轮对话)
- ✅ 即时反馈("正在查询")
测试验证
- 项目: test0102
- REDCap PID: 16, 11条记录
- Dify Dataset ID:
b49595b2-bf71-4e47-9988-4aa2816d3c6f - 文档: 研究方案、CRF表格(2个文件,已处理)
- 场景1: 查询ID 7患者信息(REDCap)→ ✅ 完全匹配真实数据
- 场景2: 查询研究排除标准(Dify)→ ✅ 基于文档准确回答
- 场景3: 查询CRF观察指标(Dify)→ ✅ 基于文档准确回答
- 场景4: 统计入组人数(REDCap)→ ✅ 准确统计11人
- 结果: ✅ 所有测试通过,无编造
详细记录
维护者:IIT Manager开发团队
最后更新:2026-01-03
文档状态:✅ Phase 1.5已完成