Files
AIclinicalresearch/docs/03-业务模块/IIT Manager Agent/04-开发计划/02-核心引擎实现指南.md
HaHafeng 0c590854b5 docs(iit): Add IIT Manager Agent V2.9 development plan with multi-agent architecture
Features:
- Add V2.9 enhancements: Cron Skill, User Profiling, Feedback Loop, Multi-Intent Handling
- Create modular development plan documents (database, engines, services, memory, tasks)
- Add V2.5/V2.6/V2.8/V2.9 design documents for architecture evolution
- Add system design white papers and implementation guides

Architecture:
- Dual-Brain Architecture (SOP + ReAct engines)
- Three-layer memory system (Flow Log, Hot Memory, History Book)
- ProfilerService for personalized responses
- SchedulerService with Cron Skill support

Also includes:
- Frontend nginx config updates
- Backend test scripts for WeChat signature
- Database backup files

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-05 22:33:26 +08:00

20 KiB
Raw Blame History

IIT Manager Agent 核心引擎实现指南

版本: V2.9
更新日期: 2026-02-05
关联文档: IIT Manager Agent V2.6 综合开发计划

V2.9 更新ReActEngine Prompt 优化支持多意图处理Chain of Thought


1. 引擎总览

引擎 职责 执行方式 Phase
HardRuleEngine 执行硬规则JSON Logic CPU 执行,无 LLM 1
SoftRuleEngine 执行软指令LLM 推理) LLM + 工具调用 2
SopEngine SOP 状态机调度 编排 Hard/Soft 引擎 2
ReActEngine 多步推理(思考-行动-观察) LLM + 循环推理 5
┌─────────────────────────────────────────────────────────────────────┐
│                         SopEngine (状态机)                           │
│  ┌────────────────────────────────────────────────────────────────┐ │
│  │ 节点A: HardRuleEngine  →  节点B: SoftRuleEngine  →  节点C: ... │ │
│  └────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                     ToolsService (工具层)                            │
└─────────────────────────────────────────────────────────────────────┘

2. HardRuleEngine - 硬规则引擎

文件路径: backend/src/modules/iit-manager/engines/HardRuleEngine.ts

2.1 核心职责

  • 执行 JSON Logic 规则,无需 LLM
  • 返回结构化的违规列表
  • 执行时间 < 100ms

2.2 完整实现

import jsonLogic from 'json-logic-js';

export interface HardRule {
  field: string;
  logic: object;  // JSON Logic 表达式
  message: string;
  severity?: 'error' | 'warning' | 'info';
}

export interface RuleResult {
  field: string;
  message: string;
  severity: string;
  value?: any;
}

export class HardRuleEngine {
  /**
   * 执行硬规则检查
   * @param rules 规则数组
   * @param data 待检查的数据
   * @returns 违规列表(空数组表示全部通过)
   */
  run(rules: HardRule[], data: Record<string, any>): RuleResult[] {
    const errors: RuleResult[] = [];
    
    for (const rule of rules) {
      try {
        const passed = jsonLogic.apply(rule.logic, data);
        
        if (!passed) {
          errors.push({
            field: rule.field,
            message: rule.message,
            severity: rule.severity || 'error',
            value: data[rule.field]
          });
        }
      } catch (error) {
        // 规则执行异常,记录但不中断
        errors.push({
          field: rule.field,
          message: `规则执行异常: ${error.message}`,
          severity: 'error'
        });
      }
    }
    
    return errors;
  }

  /**
   * 检查是否全部通过
   */
  isAllPassed(rules: HardRule[], data: Record<string, any>): boolean {
    return this.run(rules, data).length === 0;
  }
}

2.3 JSON Logic 常用表达式

// 范围检查
{ "and": [
  { ">=": [{ "var": "age" }, 18] },
  { "<=": [{ "var": "age" }, 75] }
]}

// 非空检查
{ "!!": { "var": "informed_consent_date" } }

// 日期比较(需自定义操作符)
{ "<=": [{ "var": "icf_date" }, { "var": "enrollment_date" }] }

// 枚举值检查
{ "in": [{ "var": "ecog" }, [0, 1, 2]] }

// 字符串包含
{ "in": ["肺炎", { "var": "medical_history" }] }

3. SoftRuleEngine - 软规则引擎

文件路径: backend/src/modules/iit-manager/engines/SoftRuleEngine.ts

3.1 核心职责

  • 执行需要 LLM 推理的软指令
  • 支持工具调用
  • 实现自我修正回路(最多 3 次重试)

3.2 完整实现

import { LLMFactory } from '../../common/llm/adapters/LLMFactory';
import { ToolsService } from '../services/ToolsService';

export interface SoftRuleResult {
  passed: boolean;
  reason: string;
  confidence?: number;
  evidence?: any;
}

export class SoftRuleEngine {
  private llm = LLMFactory.create('qwen');
  private tools: ToolsService;

  constructor(tools: ToolsService) {
    this.tools = tools;
  }

  /**
   * 执行软指令,带自我修正回路
   */
  async runWithRetry(
    instruction: string,
    context: any,
    maxRetries: number = 3
  ): Promise<SoftRuleResult> {
    const systemPrompt = `你是一个临床研究质控专家。请根据以下指令和数据进行判断。

指令: ${instruction}

请返回 JSON 格式:
{
  "passed": true/false,
  "reason": "判断理由",
  "confidence": 0.0-1.0,
  "evidence": "支持判断的证据"
}`;

    let history: Message[] = [
      { role: 'system', content: systemPrompt },
      { role: 'user', content: `数据: ${JSON.stringify(context)}` }
    ];
    
    for (let i = 0; i < maxRetries; i++) {
      const response = await this.llm.chat(history, {
        tools: this.tools.getToolDefinitions()
      });
      
      // AI 决定调用工具
      if (response.toolCalls && response.toolCalls.length > 0) {
        for (const toolCall of response.toolCalls) {
          try {
            const result = await this.tools.executeTool(
              toolCall.name,
              toolCall.args
            );
            history.push({
              role: 'tool',
              toolCallId: toolCall.id,
              content: JSON.stringify(result)
            });
          } catch (error) {
            // ⚠️ 自我修正:告诉 LLM 它错了
            history.push({
              role: 'user',
              content: `工具调用失败: ${error.message}。请检查参数并重试,或直接根据已有信息判断。`
            });
            continue;
          }
        }
      } else {
        // AI 返回最终答案
        return this.parseResult(response.content);
      }
    }
    
    return { 
      passed: false, 
      reason: '多次重试后仍失败,需人工复核',
      confidence: 0 
    };
  }

  private parseResult(content: string): SoftRuleResult {
    try {
      // 提取 JSON可能被 Markdown 包裹)
      const jsonMatch = content.match(/\{[\s\S]*\}/);
      if (jsonMatch) {
        return JSON.parse(jsonMatch[0]);
      }
    } catch (e) {
      // 解析失败
    }
    
    // 回退:尝试从文本推断
    const passed = content.includes('通过') || content.includes('符合');
    return {
      passed,
      reason: content.slice(0, 200),
      confidence: 0.5
    };
  }
}

4. SopEngine - SOP 状态机引擎

文件路径: backend/src/modules/iit-manager/engines/SopEngine.ts

4.1 核心职责

  • 解析 Skill 配置中的 SOP 流程图
  • 按顺序执行节点Hard → Soft → Human Review
  • 实现状态持久化(防止服务重启丢失进度)
  • 实现 SUSPENDED 挂起机制(解决"死锁"风险)

4.2 类型定义

interface SopConfig {
  name: string;
  start_node: string;
  nodes: {
    [nodeId: string]: SopNode;
  };
}

interface SopNode {
  type: 'hard_rule' | 'soft_instruction' | 'human_review';
  // hard_rule 类型
  rules?: HardRule[];
  // soft_instruction 类型
  instruction?: string;
  tools?: string[];
  // human_review 类型
  description?: string;
  // 流转
  on_pass: string;
  on_fail: string;
  on_approve?: string;  // human_review 专用
  on_reject?: string;   // human_review 专用
  on_error?: string;
}

interface SopResult {
  status: 'COMPLETED' | 'SUSPENDED' | 'FAILED';
  finalState: string;
  trace: TraceItem[];
  taskRunId?: string;
  message?: string;
}

interface TraceItem {
  node: string;
  timestamp: Date;
  result?: any;
}

4.3 完整实现

import PgBoss from 'pg-boss';
import { prisma } from '../../common/prisma';
import { HardRuleEngine } from './HardRuleEngine';
import { SoftRuleEngine } from './SoftRuleEngine';

export class SopEngine {
  private hardEngine: HardRuleEngine;
  private softEngine: SoftRuleEngine;
  private pgBoss: PgBoss;

  constructor(
    hardEngine: HardRuleEngine,
    softEngine: SoftRuleEngine,
    pgBoss: PgBoss
  ) {
    this.hardEngine = hardEngine;
    this.softEngine = softEngine;
    this.pgBoss = pgBoss;
  }

  /**
   * 执行 SOP 流程
   */
  async run(
    skillConfig: SopConfig,
    data: any,
    taskRunId?: string
  ): Promise<SopResult> {
    // 创建或恢复任务记录
    const task = taskRunId
      ? await this.resumeTask(taskRunId)
      : await this.createTask(skillConfig, data);

    let currentNodeId = task.currentNode || skillConfig.start_node;
    let context = { ...data };
    const trace: TraceItem[] = task.trace ? JSON.parse(task.trace) : [];

    while (currentNodeId && !currentNodeId.startsWith('end')) {
      const node = skillConfig.nodes[currentNodeId];
      trace.push({ node: currentNodeId, timestamp: new Date() });

      // ⚠️ 每步持久化,防止服务重启丢失进度
      await prisma.iitTaskRun.update({
        where: { id: task.id },
        data: {
          currentNode: currentNodeId,
          trace: JSON.stringify(trace),
          updatedAt: new Date()
        }
      });

      // 检查是否需要人工介入
      if (node.type === 'human_review') {
        return await this.handleHumanReview(task.id, node, data, trace);
      }

      // 执行节点
      let result: NodeResult;
      try {
        if (node.type === 'hard_rule') {
          const errors = this.hardEngine.run(node.rules || [], context);
          result = { passed: errors.length === 0, errors };
        } else {
          result = await this.softEngine.runWithRetry(
            node.instruction || '',
            context
          );
        }
      } catch (error) {
        // 执行异常,走 on_error 分支
        currentNodeId = node.on_error || 'end_error';
        continue;
      }

      // 记录违规
      if (!result.passed) {
        await this.savePendingAction(task.id, data.recordId, result);
      }

      // 状态流转
      currentNodeId = result.passed ? node.on_pass : node.on_fail;
    }

    // 完成任务
    await prisma.iitTaskRun.update({
      where: { id: task.id },
      data: {
        status: 'COMPLETED',
        currentNode: currentNodeId,
        completedAt: new Date()
      }
    });

    return { 
      status: 'COMPLETED',
      finalState: currentNodeId, 
      trace 
    };
  }

  /**
   * ⚠️ 处理人工复核节点 - SUSPENDED 挂起机制
   */
  private async handleHumanReview(
    taskRunId: string,
    node: SopNode,
    data: any,
    trace: TraceItem[]
  ): Promise<SopResult> {
    // 挂起任务,不再占用 Worker
    await prisma.iitTaskRun.update({
      where: { id: taskRunId },
      data: {
        status: 'SUSPENDED',
        suspendedAt: new Date(),
        resumeCallback: node.on_approve,
        rejectCallback: node.on_reject || 'end_rejected'
      }
    });

    // 通知相关人员
    await this.notifyReviewRequired(data, node);

    return {
      status: 'SUSPENDED',
      finalState: 'human_review',
      trace,
      taskRunId,
      message: '任务已挂起,等待人工复核'
    };
  }

  /**
   * ⚠️ 唤醒机制 - CRC 确认后继续执行
   */
  async resumeFromSuspended(
    taskRunId: string,
    decision: 'approve' | 'reject'
  ): Promise<void> {
    const task = await prisma.iitTaskRun.findUnique({
      where: { id: taskRunId }
    });

    if (!task || task.status !== 'SUSPENDED') {
      throw new Error('任务不在挂起状态');
    }

    const nextNode = decision === 'approve'
      ? task.resumeCallback
      : task.rejectCallback || 'end_rejected';

    // 更新状态
    await prisma.iitTaskRun.update({
      where: { id: taskRunId },
      data: {
        status: 'RUNNING',
        currentNode: nextNode,
        resumedAt: new Date()
      }
    });

    // 创建新的 Job 继续执行
    await this.pgBoss.send('sop-continue', { taskRunId });
  }

  // ... 辅助方法省略
}

5. ReActEngine - 多步推理引擎

文件路径: backend/src/modules/iit-manager/engines/ReActEngine.ts

5.1 核心职责

  • 处理模糊查询,支持多步推理
  • 循环执行"思考 → 行动 → 观察"
  • 只允许调用只读工具(安全约束)
  • 只返回 Final Answer(解决"多嘴"问题)
  • V2.9 新增:支持多意图处理(用户一句话包含多个任务)

5.2 安全约束

// 工具白名单ReAct 只能调用只读工具
private readonly READONLY_TOOLS = [
  'read_clinical_data',
  'search_protocol',
  'get_project_stats',
  'check_visit_window'
];

// 资源限制
private maxIterations = 5;   // 最大迭代次数
private maxTokens = 4000;    // Token 预算

5.3 完整实现

import { LLMFactory } from '../../common/llm/adapters/LLMFactory';
import { ToolsService } from '../services/ToolsService';
import { prisma } from '../../common/prisma';

export interface ReActResult {
  success: boolean;
  content: string;
  trace: TraceItem[];
}

interface TraceItem {
  iteration: number;
  thought?: string;
  action?: string;
  observation?: string;
}

export class ReActEngine {
  private llm = LLMFactory.create('qwen');
  private tools: ToolsService;
  
  private readonly READONLY_TOOLS = [
    'read_clinical_data',
    'search_protocol',
    'get_project_stats',
    'check_visit_window'
  ];
  
  private maxIterations = 5;
  private maxTokens = 4000;
  private tokenCounter = 0;

  constructor(tools: ToolsService) {
    this.tools = tools;
  }

  async run(query: string, context: AgentContext): Promise<ReActResult> {
    this.tokenCounter = 0;
    
    // V2.9 优化:支持多意图处理的 Prompt
    const systemPrompt = `你是一个临床研究智能助手。你可以使用以下工具回答问题:
${this.formatToolDescriptions(this.READONLY_TOOLS)}

请按照 ReAct 模式思考:
1. 思考:分析问题,决定下一步
2. 行动:调用工具获取信息
3. 观察:查看工具返回结果
4. 重复以上步骤,直到能回答用户问题

## 多任务处理原则 (V2.9)

当用户的消息包含多个任务时,请遵循以下步骤:

**Step 1: 意图拆解**
- 识别用户消息中的所有独立意图
- 按依赖关系排序:先处理前置任务
- 示例:"帮我查一下 P003 的 ECOG然后录入今天的访视数据"
  → 意图1: 查询 P003 ECOG只读
  → 意图2: 录入访视数据(需要引导)

**Step 2: 顺序执行**
- 每次只聚焦处理一个意图
- 完成后明确告知用户,并继续下一个
- 遇到写操作,暂停并引导用户确认

**Step 3: 统一回复**
- 在最终回复中按顺序总结所有任务结果
- 格式:
  1. [任务1] 已完成 / 结果:...
  2. [任务2] 需要确认 / 引导操作

注意:你只能查询数据,不能修改数据。如果用户需要修改操作,请引导他们使用正式的质控流程。`;

    let messages: Message[] = [
      { role: 'system', content: systemPrompt },
      { role: 'user', content: query }
    ];
    
    const trace: TraceItem[] = [];
    
    for (let i = 0; i < this.maxIterations; i++) {
      // Token 预算检查
      if (this.tokenCounter > this.maxTokens) {
        return {
          success: false,
          content: '抱歉,这个问题比较复杂,请尝试更具体的描述。',
          trace
        };
      }
      
      const response = await this.llm.chat(messages, {
        tools: this.getReadonlyToolDefinitions()
      });
      
      this.tokenCounter += response.usage?.total_tokens || 0;
      trace.push({ iteration: i, thought: response.content });
      
      // AI 决定结束,返回 Final Answer
      if (!response.toolCalls || response.toolCalls.length === 0) {
        return { success: true, content: response.content, trace };
      }
      
      // 执行工具调用
      let errorCount = 0;
      for (const toolCall of response.toolCalls) {
        // ⚠️ 安全检查:只允许只读工具
        if (!this.READONLY_TOOLS.includes(toolCall.name)) {
          messages.push({
            role: 'tool',
            toolCallId: toolCall.id,
            content: `错误:工具 ${toolCall.name} 不在允许列表中。你只能使用只读工具查询数据。如果需要修改数据,请引导用户使用"质控"命令。`
          });
          errorCount++;
          continue;
        }
        
        try {
          const result = await this.tools.executeTool(toolCall.name, toolCall.args);
          messages.push({
            role: 'tool',
            toolCallId: toolCall.id,
            content: JSON.stringify(result)
          });
          trace[i].observation = JSON.stringify(result).slice(0, 200);
        } catch (error) {
          errorCount++;
          messages.push({
            role: 'tool',
            toolCallId: toolCall.id,
            content: `工具调用失败: ${error.message}`
          });
        }
      }
      
      // ⚠️ 幻觉熔断:连续 2 次工具调用失败
      if (errorCount >= 2) {
        return {
          success: false,
          content: '抱歉,我遇到了一些技术问题,无法获取相关信息。请稍后重试或联系管理员。',
          trace
        };
      }
    }
    
    return {
      success: false,
      content: '抱歉,我无法在有限步骤内完成这个查询。请尝试更具体的问题。',
      trace
    };
  }

  /**
   * ⚠️ 保存 Trace 到数据库(仅供 Admin 调试)
   */
  async saveTrace(
    projectId: string,
    userId: string,
    query: string,
    result: ReActResult
  ): Promise<void> {
    await prisma.iitAgentTrace.create({
      data: {
        projectId,
        userId,
        query,
        trace: result.trace,
        tokenUsage: this.tokenCounter,
        success: result.success,
        createdAt: new Date()
      }
    });
  }

  private getReadonlyToolDefinitions() {
    return this.tools.getToolDefinitions()
      .filter(t => this.READONLY_TOOLS.includes(t.name));
  }

  private formatToolDescriptions(toolNames: string[]): string {
    return toolNames.map(name => {
      const def = this.tools.getToolDefinition(name);
      return `- ${name}: ${def?.description || ''}`;
    }).join('\n');
  }
}

6. 引擎集成示意

// 初始化
const hardEngine = new HardRuleEngine();
const toolsService = new ToolsService(redcapAdapter, difyClient);
const softEngine = new SoftRuleEngine(toolsService);
const sopEngine = new SopEngine(hardEngine, softEngine, pgBoss);
const reactEngine = new ReActEngine(toolsService);

// 在 ChatService 中使用
async handleMessage(userId: string, message: string) {
  const intent = await this.intentService.detect(message);
  
  if (intent.type === 'QC_TASK') {
    // 走 SOP 引擎
    return await this.sopEngine.run(skillConfig, data);
  } else if (intent.type === 'QA_QUERY') {
    // 走 ReAct 引擎
    const result = await this.reactEngine.run(message, context);
    // ⚠️ 只返回 Final Answer不返回中间思考
    await this.reactEngine.saveTrace(projectId, userId, message, result);
    return result.content;
  }
}

文档维护人AI Agent
最后更新2026-02-05