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>
This commit is contained in:
2026-02-05 22:33:26 +08:00
parent 4b9b90ffb8
commit 0c590854b5
27 changed files with 7279 additions and 7 deletions

View File

@@ -0,0 +1,341 @@
# **IIT Manager Agent V2.2 落地实施指南:极简架构版 (融合 Moltbot)**
**文档版本:** V2.2 (Moltbot Enhanced)
**日期:** 2026-02-03
**适用对象:** 2人高效开发团队
**核心原则:** **Postgres-Only**, **No New Components**, **Service-First**, **Proactive & Personalized**
**变更说明:** 在 V2.1 基础上,借鉴 Moltbot (Clawbot) 的主动性与记忆机制,增加了用户偏好存储、每日早报 Skill 和自我修正回路。
## **1\. 核心架构:回归本质 (The Pragmatic Architecture)**
我们将“Brain-Hand-Tool”模型映射到现有的 Node.js \+ Postgres 技术栈上。
graph TD
subgraph "Layer 3: 数字化大脑 (The Brain)"
DB\[(Postgres DB)\]
DB \--\>|加载 Skills & Preferences| Engine\[QcEngineService\]
subgraph "混合双引擎"
Engine \--\>|1. 执行 JSON Logic| Logic\[Engine A (硬规则)\]
Engine \--\>|2. 组装 Prompt (含偏好)| LLM\[Engine B (软指令)\]
end
end
subgraph "Layer 2: 伪 MCP 接口层 (The Hand)"
LLM \--\>|3. 调用| Tools\[ToolsService (Class)\]
Logic \--\>|3. 调用| Tools
Tools \--\>|read\_data| Tool1\[RedcapAdapter\]
Tools \--\>|search\_doc| Tool2\[VectorSearchService\]
Tools \--\>|send\_msg| Tool3\[WechatService\]
end
subgraph "Layer 1: 基础设施 (The Tool)"
Tool1 \--\> REDCap
Tool2 \--\> pgvector
Tool3 \--\> WeCom
end
## **2\. 数据层设计Skills 即数据 (Skills as Data)**
不再使用 OSS 存文件,直接使用 Postgres iit\_schema 存储结构化配置。**新增用户偏好表。**
### **2.1 数据库 Schema**
// schema.prisma
// 核心技能表
model IitSkill {
id String @id @default(uuid())
projectId String // 绑定项目 ID (如 PID=16)
formName String // 绑定表单 (如 demographics) 或 场景 (如 daily\_briefing)
// 核心配置 (JSON 类型)
// 包含 hard\_rules, soft\_instructions, schedule (cron表达式)
config Json
isActive Boolean @default(true)
version Int @default(1)
updatedAt DateTime @updatedAt
@@unique(\[projectId, formName\])
@@map("iit\_skills")
@@schema("iit\_schema")
}
// \[New\] 用户偏好表 (Moltbot 记忆机制)
model IitUserPreference {
id String @id @default(uuid())
userId String // 绑定企业微信 UserID
projectId String // 绑定项目
// 偏好配置 (JSON)
// 例如: { "report\_style": "concise", "notification\_time": "08:30" }
preferences Json
updatedAt DateTime @updatedAt
@@unique(\[userId, projectId\])
@@map("iit\_user\_preferences")
@@schema("iit\_schema")
}
### **2.2 JSON 配置范例 (Skill)**
**范例 1质控 Skill**
{
"description": "人口学表单质控",
"hard\_rules": \[
{
"field": "age",
"logic": { "\>=": \[{ "var": "age" }, 18\] },
"message": "年龄必须 \>= 18 岁"
}
\],
"soft\_instructions": \[
{
"instruction": "检查备注字段。如果包含'无法签署知情同意',请标记违规。",
"tools": \["read\_clinical\_data"\]
}
\]
}
**范例 2\[New\] 每日早报 Skill (Moltbot 主动性)**
{
"description": "每日项目进展早报",
"schedule": "0 30 8 \* \* \*", // 每天 08:30 触发 (pg-boss)
"soft\_instructions": \[
{
"instruction": "请统计昨天的入组人数、待处理质疑数。生成一段简报发给 PI。注意参考用户的 report\_style 偏好。",
"tools": \["get\_project\_stats", "send\_wechat\_msg"\]
}
\]
}
## **3\. 服务层设计:伪 MCP 实现 (Service-First)**
不要引入 @modelcontextprotocol/sdk。创建一个标准的 Service 类来管理工具。
### **3.1 ToolsService.ts 实现规范**
// backend/src/modules/iit-manager/services/ToolsService.ts
import { RedcapAdapter } from '../adapters/RedcapAdapter';
import { VectorSearchService } from '@/common/rag/vectorSearchService';
// 定义工具接口 (为了给 LLM 生成菜单)
export const TOOL\_DEFINITIONS \= \[
{
name: "read\_clinical\_data",
description: "读取REDCap临床数据...",
parameters: { ... }
},
{
name: "update\_user\_preference", // \[New\] 更新偏好工具
description: "更新用户的偏好设置,如报告风格、通知时间。",
parameters: {
type: "object",
properties: {
key: { type: "string", description: "偏好键名,如 report\_style" },
value: { type: "string", description: "偏好值,如 concise (简洁) 或 detailed (详细)" }
}
}
}
\];
export class ToolsService {
constructor(
private redcap: RedcapAdapter,
private rag: VectorSearchService,
private prisma: PrismaClient
) {}
// 统一执行入口 (LLM 只认识这个)
async executeTool(name: string, args: any) {
switch (name) {
case 'read\_clinical\_data':
return this.redcap.exportRecords(args);
case 'update\_user\_preference':
// \[New\] 实现记忆更新
return this.updatePreference(args);
case 'raise\_query':
return this.savePendingAction(args);
default:
throw new Error(\`未知工具: ${name}\`);
}
}
}
## **4\. 业务层设计:混合引擎调度 (含自我修正)**
### **4.1 执行逻辑 (The Pipeline)**
// backend/src/modules/iit-manager/services/QcEngineService.ts
export class QcEngineService {
async runQualityCheck(projectId: string, formName: string, data: any, userId?: string) {
// 1\. 加载 Skill 和 用户偏好
const skill \= await prisma.iitSkill.findUnique({...});
const userPref \= userId ? await prisma.iitUserPreference.findUnique({ where: { userId, projectId } }) : null;
if (\!skill) return;
// 2\. 运行 Engine A (硬规则)
const engineA \= new HardRuleEngine();
const hardErrors \= engineA.run(skill.config.hard\_rules, data);
if (hardErrors.length \> 0\) {
await this.saveShadowState(hardErrors);
return;
}
// 3\. 运行 Engine B (软指令) \- \[New\] 注入偏好 Context
const engineB \= new SoftRuleEngine(this.llm, this.toolsService);
// 将偏好注入 Prompt: "用户偏好简洁的报告,请不要废话。"
const context \= {
...data,
user\_preferences: userPref?.preferences
};
// \[New\] 开启自我修正回路 (Self-Correction Loop)
const softSuggestions \= await engineB.runWithRetry(skill.config.soft\_instructions, context, 3);
if (softSuggestions) {
await this.saveShadowState(softSuggestions);
}
}
}
### **4.2 自我修正机制 (Self-Correction Loop)**
借鉴 Moltbot当工具调用失败时不报错而是让 LLM 重试。
// SoftRuleEngine.ts (伪代码)
async runWithRetry(instruction, context, maxRetries \= 3\) {
let history \= \[...\];
for (let i \= 0; i \< maxRetries; i++) {
const response \= await llm.chat(history);
if (response.hasToolCall) {
try {
const result \= await tools.executeTool(response.toolName, response.args);
history.push({ role: 'tool', content: result });
} catch (error) {
// \[New\] 捕获错误,喂回给 LLM
console.warn(\`Tool Error: ${error.message}\`);
history.push({
role: 'user',
content: \`工具调用失败: ${error.message}。请检查参数并重试。\`
});
continue; // 让 LLM 重新思考
}
} else {
return response.content;
}
}
}
## **5\. 实施路线图 (2人团队专属)**
### **✅ Week 1: 基础建设 (Engine A)**
1. 创建 iit\_skills 和 iit\_user\_preferences 表。
2. 引入 json-logic-js 库。
3. 实现 QcEngineService 的基础骨架,只跑硬规则。
### **🚧 Week 2: AI 接入与主动性 (Engine B)**
1. 创建 ToolsService实现基础工具。
2. 实现 SoftRuleEngine 的 **自我修正回路**
3. **里程碑**:配置 daily\_briefing Skill利用 pg-boss 定时触发,早上 8:30 收到测试推送。
### **📋 Week 3: 界面与优化**
1. 开发 Admin 页面 (JSON Editor) 编辑 Skill。
2. 完善影子状态 (Shadow State) 审核界面。
## **6\. 多轮对话与开放式问答支持**
### **6.1 核心策略Intent Routing \+ General Skill**
不要把所有逻辑都写死在质控里。我们增加一个特殊的 Skillgeneral\_qa。
#### **A. 配置 general\_qa Skill**
在数据库插入一条 formName \= 'general\_chat' 的记录:
{
"description": "通用问答与查询助手",
"soft\_instructions": \[
{
"instruction": "你是本项目的智能助手。你可以回答关于项目进度、患者数据、方案细节的问题。请根据用户意图调用工具。",
"tools": \["read\_clinical\_data", "search\_protocol", "get\_project\_stats", "update\_user\_preference"\]
}
\]
}
### **6.2 代码实现:聊天入口 (ChatService.ts)**
// backend/src/modules/iit-manager/services/ChatService.ts
export class ChatService {
constructor(
private qcEngine: QcEngineService,
private sessionMemory: SessionMemory
) {}
async handleUserMessage(userId: string, projectId: string, message: string) {
// 1\. 获取历史对话 & 用户偏好
const history \= await this.sessionMemory.get(userId);
const userPref \= await prisma.iitUserPreference.findUnique({...});
// 2\. 加载通用 Skill
const generalSkill \= await prisma.iitSkill.findUnique({
where: { projectId, formName: 'general\_chat' }
});
// 3\. 构造 Prompt (包含 History \+ Skill \+ Preferences)
const prompt \= \`
${generalSkill.config.soft\_instructions\[0\].instruction}
\[User Preferences\]: ${JSON.stringify(userPref?.preferences || {})}
历史对话:
${history.map(h \=\> \`${h.role}: ${h.content}\`).join('\\n')}
用户当前问题: ${message}
\`;
// 4\. 调用 LLM (Engine B 逻辑复用,含自我修正)
const response \= await this.qcEngine.runSoftAgent(prompt, generalSkill.config);
// 5\. 更新记忆
await this.sessionMemory.add(userId, { role: 'user', content: message });
await this.sessionMemory.add(userId, { role: 'assistant', content: response });
return response;
}
}
## **7\. Moltbot 理念借鉴总结**
| 借鉴点 | IIT Manager 落地实现 | 价值 |
| :---- | :---- | :---- |
| **主动性 (Proactivity)** | **每日早报 Skill**:利用 pg-boss 定时触发 daily\_briefing Skill主动推送企微消息。 | 让用户感觉系统是活的,而不是被动的数据库。 |
| **记忆 (Memory)** | **用户偏好表**:存储 report\_style, notification\_time 等Prompt 动态注入。 | 提供千人千面的个性化体验。 |
| **自我修正 (Self-Correction)** | **重试回路**Engine B 在工具报错时,将错误信息回传给 LLM允许其修正参数重试。 | 极大提升系统鲁棒性,减少人工运维。 |
**结论:这就是 IIT Manager Agent 的最终形态。既有医疗的严谨硬规则又有个性化的温度偏好记忆还有极简的架构Postgres-Only。开干吧**

View File

@@ -0,0 +1,158 @@
# **IIT Manager Agent V2.2:工具泛化与灵活性提升指南**
**核心观点:** 灵活性来自“工具的设计粒度”而不是“连接协议MCP”。
**解决方案:** 通过设计 3-5 个“瑞士军刀型”通用工具,替代 100 个“特种螺丝刀型”专用工具。
## **1\. 直击灵魂MCP Server 真的更灵活吗?**
你担心 V2.1 方案Service Class不够灵活让我们来做个对比
| 维度 | MCP Server 方案 | V2.1 Service Class 方案 | 真相 |
| :---- | :---- | :---- | :---- |
| **新增工具** | 在 Server 进程里写代码 \-\> 重启 Server \-\> Agent 发现新工具 | 在 Service 类里写代码 \-\> 重启 Node.js \-\> Agent 发现新工具 | **工作量完全一样**。都要写代码逻辑。 |
| **工具调用** | Agent 发 JSON \-\> 网络传输 \-\> MCP 执行 | Agent 发 JSON \-\> 内存调用 \-\> Service 执行 | **V2.1 更快**。没有网络开销。 |
| **Agent 自主性** | LLM 看到的是 tool definitions (JSON) | LLM 看到的也是 tool definitions (JSON) | **LLM 根本分不清**你是 MCP 还是 Service。 |
**结论:**
* **MCP 的灵活性在于“解耦”**:比如你可以把 REDCap 工具给别人用,或者在运行时动态加载新的 Server。
* **V2.1 的灵活性在于“敏捷”**:代码都在一个工程里,改起来最快。对于 2 人团队,**改代码 \> 调协议**。
## **2\. 你的痛点:是不是要写很多工具?**
你担心:“如果有 100 个检查点,我是不是要写 check\_age, check\_gender, check\_bmi 一百个工具?”
**绝对不需要!** 这就是我说的\*\*“工具泛化”\*\*。
### **错误的设计(特种螺丝刀)**
写一堆具体的工具,导致 Service 类无限膨胀:
* get\_patient\_age(id)
* get\_patient\_history(id)
* check\_informed\_consent(id)
* ...
### **正确的设计(瑞士军刀)**
只写 **3 个** 通用工具,就能覆盖 99% 的场景。Agent 会通过**参数**来体现灵活性。
#### **工具 1read\_clinical\_data (全能查询器)**
* **功能**:读取 REDCap 里的任意数据。
* **参数**record\_id (患者ID), fields (想查什么字段), event (哪个访视)。
* **灵活性**
* 查年龄Agent 传 fields: \["age"\]。
* 查病史Agent 传 fields: \["medical\_history"\]。
* **你只需要写这一个函数Agent 就可以查任何东西。**
#### **工具 2eval\_logic (逻辑计算器)**
* **功能**:处理复杂的数值计算或逻辑判断(利用 json-logic
* **场景**Agent 算不清 BMI可以调这个工具帮它算。
#### **工具 3manage\_issue (通用反馈器)**
* **功能**:包括提出质疑、发送提醒、记录日志。
* **参数**type (QUERY | NOTIFY | LOG), message (内容), severity (严重程度)。
## **3\. 代码实现:如何实现“瑞士军刀”?**
在你的 ToolsService.ts 中,只需要实现这几个通用方法。
// backend/src/modules/iit-manager/services/ToolsService.ts
export const UNIVERSAL\_TOOL\_DEFS \= \[
{
name: "read\_clinical\_data",
description: "通用数据查询工具。当需要检查患者的某项指标时使用。",
parameters: {
type: "object",
properties: {
record\_id: { type: "string" },
// 关键:让 Agent 自己决定查什么字段
fields: {
type: "array",
items: { type: "string" },
description: "需要查询的REDCap变量名列表如 \['age', 's\_cr', 'ae\_desc'\]"
}
},
required: \["record\_id", "fields"\]
}
},
{
name: "manage\_issue",
description: "通用问题处理工具。用于提出质疑或发送通知。",
parameters: {
type: "object",
properties: {
record\_id: { type: "string" },
action\_type: { type: "string", enum: \["RAISE\_QUERY", "SEND\_WECHAT"\] },
message: { type: "string" }
},
required: \["record\_id", "action\_type", "message"\]
}
}
\];
export class ToolsService {
constructor(private redcapAdapter: RedcapAdapter) {}
async execute(name: string, args: any) {
switch (name) {
case 'read\_clinical\_data':
// 一个函数,通过参数变化应对无限需求
return await this.redcapAdapter.exportRecords({
recordId: args.record\_id,
fields: args.fields // \<--- 动态字段
});
case 'manage\_issue':
if (args.action\_type \=== 'RAISE\_QUERY') {
return await this.saveShadowQuery(args);
} else {
return await this.sendWechat(args);
}
}
}
}
## **4\. 场景演示Agent 的自主性如何体现?**
哪怕你只提供了 2 个工具Agent 依然表现得像个专家,因为**Skill (Prompt)** 在指导它如何组合使用这些工具。
### **场景 A检查肝功能**
* **Skill 配置**: "instruction": "检查 ALT 和 AST 是否超过正常值 3 倍。"
* **Agent 思考**: "我要查肝功能。"
* **Agent 行动**: 调用 read\_clinical\_data(fields=\['alt', 'ast'\])。
* **系统响应**: {"alt": 150, "ast": 40}
* **Agent 判断**: "ALT 150 \> 40\*3。违规。"
* **Agent 行动**: 调用 manage\_issue(action\_type='RAISE\_QUERY', message='ALT异常')。
### **场景 B检查入组年龄**
* **Skill 配置**: "instruction": "确保患者年龄 \> 18。"
* **Agent 思考**: "我要查年龄。"
* **Agent 行动**: 调用 read\_clinical\_data(fields=\['age'\])。
* **系统响应**: {"age": 16}
* **Agent 行动**: 调用 manage\_issue(action\_type='RAISE\_QUERY', message='年龄不合规')。
**看到没有?**
你**没有写** check\_liver 和 check\_age 两个函数。
你只写了一个 read\_clinical\_data。
**Agent 的大脑Prompt** \+ **通用工具Tool** 实现了无限的灵活性。
## **5\. 总结**
1. **MCP Server 不是灵活性的来源**:它只是一个连接标准。如果你的工具设计得烂(粒度太细),用 MCP 也一样累。
2. **通用参数是王道**:不要写“查年龄”的工具,要写“查字段”的工具。让 Agent 填参数,这就是最大的灵活性。
3. **V2.1 极简版足够强**:只要你的 ToolsService 按照“瑞士军刀”的思路去设计2 个开发人员维护 100 个项目的逻辑完全没有压力。
**行动建议:**
按照本文档的 **第 3 节**,把你的 ToolsService 重构一下,只保留 3-5 个通用方法。然后去测试一下 Agent 是否能聪明地自己填参数。

View File

@@ -0,0 +1,162 @@
# **IIT Manager Agent V2.3:健壮性设计与最佳实践**
**核心目标:** 在极简架构下,解决 LLM 调用工具时的幻觉、参数错误和无结果问题。
**适用场景:** 2人团队Node.js Service Class 架构。
## **1\. 核心风险分析LLM 会犯什么错?**
在让 Agent 调用 read\_clinical\_data 等工具时,通常会发生以下三类错误:
| 错误类型 | 场景举例 | 后果 |
| :---- | :---- | :---- |
| **参数幻觉** | LLM 调用 read\_data(id="P001", fields=\["gender"\]),但 REDCap 里字段名叫 sex。 | API 返回空或报错,用户觉得 AI 很蠢。 |
| **意图漂移** | 用户问:“今天天气怎么样?” LLM 试图去 REDCap 查天气。 | 工具调用失败,浪费 Token。 |
| **格式错误** | LLM 返回的 JSON 少了一个括号,或者参数类型不对(字符串给了数字)。 | 程序 Crash前端报错。 |
## **2\. 解决方案:三层防御体系 (The Defense Layer)**
不需要 MCP Server我们在 Service Class 里加三层防御即可。
### **第一层:模糊映射 (The Translator)**
**解决:** 字段名对不上的问题。
**原理:** 别指望 LLM 能记住 REDCap 几千个变量名。我们在代码里做一个“模糊翻译器”。
// backend/src/modules/iit-manager/services/ToolsService.ts
class ToolsService {
// 1\. 定义常用字段映射字典
private fieldMapping \= {
"gender": "sex",
"sex": "sex",
"性别": "sex",
"age": "age\_calculated",
"年龄": "age\_calculated",
"history": "medical\_history\_desc",
"病史": "medical\_history\_desc"
};
async executeTool(name: string, args: any) {
if (name \=== 'read\_clinical\_data') {
// 🛡️ 自动纠错逻辑
const correctedFields \= args.fields.map(f \=\> {
// 如果字典里有,就替换;没有就保留原样
return this.fieldMapping\[f.toLowerCase()\] || f;
});
console.log(\`\[Auto-Fix\] ${args.fields} \-\> ${correctedFields}\`);
return this.redcapAdapter.exportRecords({
...args,
fields: correctedFields
});
}
}
}
### **第二层:容错重试 (The Self-Correction Loop)**
**解决:** LLM 调用失败直接报错给用户的问题。
**原理:** 当工具报错时,**不要直接抛给用户**,而是把错误信息喂回给 LLM让它重试。这是 Agent 能够“甚至比人更聪明”的关键。
// backend/src/modules/iit-manager/agents/QcAgent.ts
async function runAgentLoop(prompt: string, maxRetries \= 3\) {
let history \= \[{ role: 'user', content: prompt }\];
for (let i \= 0; i \< maxRetries; i++) {
// 1\. LLM 思考
const response \= await llm.chat(history);
// 2\. 如果没有调用工具,直接返回结果
if (\!response.hasToolCall) return response.content;
// 3\. 尝试执行工具
try {
const result \= await toolsService.execute(response.toolName, response.args);
// 4\. ✅ 成功:把结果喂给 LLM继续思考
history.push({ role: 'tool', content: JSON.stringify(result) });
} catch (error) {
// 5\. ❌ 失败:触发自我修正机制
console.warn(\`\[Agent Error\] 工具调用失败: ${error.message}\`);
// 关键步骤:告诉 LLM 它错了,让它重试
history.push({
role: 'user',
content: \`系统报错:${error.message}。请检查你的参数(例如字段名是否存在),然后重试。\`
});
// 循环继续LLM 会在下一次迭代中修正参数
}
}
return "抱歉,系统尝试多次后仍然无法获取数据,请联系管理员。";
}
### **第三层:空结果兜底 (The Fallback)**
**解决:** 查不到数据时体验不好的问题。
**原理:** 如果 REDCap 返回空,工具层要返回一段“人话”,而不是 null。
// ToolsService.ts
async execute(name: string, args: any) {
// ...
const data \= await this.redcapAdapter.exportRecords(...);
if (\!data || data.length \=== 0\) {
// 🛡️ 友好的空结果返回
return {
status: "empty",
message: \`未找到 ID 为 ${args.record\_id} 的患者数据。请确认 ID 是否正确。\`
};
}
return data;
}
## **3\. 最佳实践:如何应对未知问题?**
### **3.1 限制工具的“射程”**
不要给 LLM 一个 sql\_query 工具让它随便查库。
**最佳实践**:只提供 read\_clinical\_data(record\_id, fields)。
* 如果用户问“天气怎么样”LLM 发现没有 get\_weather 工具,它自己就会回答:“抱歉,我无法查询天气,我只能查询临床数据。”(这是 LLM 的自带能力)。
### **3.2 System Prompt 的“紧箍咒”**
在 qc\_config.json 的 soft\_instructions 或者全局 System Prompt 中,明确边界。
你是一个临床研究助手。
1\. 你只能回答与【肺癌研究】相关的问题。
2\. 如果用户问无关问题(如天气、股票),请礼貌拒绝。
3\. 在调用 \`read\_clinical\_data\` 时,请务必使用准确的英文变量名。如果不确定,请先调用 \`Youtube\` 查看字典。
### **3.3 调试与监控 (Admin Portal)**
你们已经有了 **运营管理端**。利用起来!
* 记录每一次 Agent 的思考过程Thinking Trace
* 如果你发现 Agent 总是把 age 拼成 agg就在 **第一层防御(映射字典)** 里加一行配置。
* 这就是\*\*“运营驱动开发”\*\*,比改代码快得多。
## **4\. 结论**
**极简方案Service Class的灵活性和健壮性完全够用。**
* **MCP Server** 只是把错误抛出来,它不会自动修错。
* **Service Class** 允许你在本地代码里直接写 try-catch 和 Retry Loop这对 2 人团队来说调试极其友好。
**行动指南:**
1. **不用担心**用户乱问Prompt 会拒绝。
2. **不用担心**参数传错,写一个简单的 Mapping 字典去拦截。
3. **不用担心**报错,实现 Self-Correction Loop出错把错误扔回给 AI让 AI 自己修。
**这就是目前 Agent 落地最务实、最抗造的模式。**

View File

@@ -0,0 +1,191 @@
# **IIT Manager Agent V2.4:架构模式选型与 SOP 状态机推荐**
**核心议题:** ReAct vs Planner vs State Machine —— 谁更适合医疗质控?
**结论前置:** 弃用纯 Planner限制使用 ReAct全面拥抱 **SOP 状态机**
## **1\. 深度解析ReAct 与 Planner 适合我们吗?**
### **1.1 ReAct (Reasoning \+ Acting)**
* **原理**Agent 像人一样“自言自语”。
* *Thought*: "我需要查患者年龄。" \-\> *Action*: read\_data \-\> *Observation*: "16岁" \-\> *Thought*: "太小了,报错。"
* **在 IIT 中的地位****它就是你的 Engine B**。
* 你现有的 runAgentLoop 代码本质上就是一个 ReAct 循环。
* **优点**:灵活,能处理突发情况(比如发现数据缺了,它知道先去查)。
* **缺点**:容易陷入死循环(反复查同一个数据),或者在思考步骤中消耗大量 Token。
* **结论****保留使用**,但必须加“紧箍咒”(最大步数限制)。
### **1.2 Planner (Query-Planner-Execute-Reflect)**
* **原理**:先写大纲,再干活。
* *Plan*: 1\. 查总人数; 2\. 算脱落率; 3\. 写周报; 4\. 反思哪里写得不好。
* **在 IIT 中的地位****仅适合“项目管理 Agent” (周报/统计)**。
* 对于“数据质控”这种秒级响应的任务Planner 太慢了(光生成计划就要 5-10 秒)。
* **缺点**:对于 2 人团队,实现一个稳定的 Planner 很难(需要维护计划状态、断点恢复)。
* **结论****质控场景禁用,报表场景可选**。
## **2\. 终极推荐SOP 状态机 (SOP-Driven State Machine)**
为什么医疗行业GCP最看重 Protocol方案因为临床研究本质上就是一个**巨大的流程图**。
我们应该让 Agent **“照着流程图走”**,而不是让它“只有个模糊目标自己瞎想”。
### **2.1 什么是 SOP 状态机架构?**
把你的 Skills JSON 进化成一个**有向图 (DAG)** 或 **流程图**
* **节点 (Node)**:一个具体的检查步骤(可以是硬规则,也可以是软指令)。
* **边 (Edge)**:下一步走哪里(通过了走 A没通过走 B
### **2.2 为什么它既稳又活?**
* **稳 (Stability)**流程是写死的先查年龄再查病史最后查合并用药。Agent 不会乱跳步骤,不会漏查。
* **活 (Flexibility)**:每个节点内部可以用 AIEngine B来灵活判断。
## **3\. 落地实施:如何改造 Skills JSON**
我们将 V2.1 的扁平配置升级为**流程配置**。
### **3.1 新的 Skill 结构 (qc\_process.json)**
{
"name": "肺癌入排质控流程",
"start\_node": "check\_age", // 入口
"nodes": {
// 节点 1: 硬规则 (CPU 执行)
"check\_age": {
"type": "hard\_rule",
"logic": { "\>=": \[{ "var": "age" }, 18\] },
"on\_pass": "check\_history", // 通过了,去查病史
"on\_fail": "end\_with\_error" // 没通过,直接结束
},
// 节点 2: 软指令 (AI 执行 \- ReAct 模式)
"check\_history": {
"type": "soft\_instruction",
"instruction": "检查病史描述,排除'间质性肺炎'。",
"tools": \["read\_clinical\_data"\],
"on\_pass": "check\_meds",
"on\_fail": "review\_required" // AI 觉得有问题,转人工复核
},
// 节点 3: 复杂逻辑 (AI 执行)
"check\_meds": {
"type": "soft\_instruction",
"instruction": "检查合并用药,排除靶向药。",
"on\_pass": "end\_success",
"on\_fail": "review\_required"
}
}
}
## **4\. 代码实现:极简状态机引擎**
不要引入 XState 这种复杂库2 人团队自己写个 while 循环就够了。
// backend/src/modules/iit-manager/services/SopEngine.ts
class SopEngine {
async run(skillConfig: any, data: any) {
let currentNodeId \= skillConfig.start\_node;
let context \= { ...data }; // 共享上下文
while (currentNodeId && currentNodeId \!== 'end') {
const node \= skillConfig.nodes\[currentNodeId\];
console.log(\`\[SOP\] Executing node: ${currentNodeId}\`);
let result;
if (node.type \=== 'hard\_rule') {
// 调用 Engine A (V2.1 的逻辑)
result \= this.runHardRule(node, context);
} else if (node.type \=== 'soft\_instruction') {
// 调用 Engine B (ReAct 模式)
result \= await this.runSoftAgent(node, context);
}
// 状态流转
if (result.passed) {
currentNodeId \= node.on\_pass;
} else {
if (node.on\_fail \=== 'end\_with\_error') {
await this.saveShadowQuery(result.message);
return;
}
currentNodeId \= node.on\_fail;
}
}
}
}
## **5\. 架构对比总结表**
| 架构模式 | 稳定性 | 灵活性 | 复杂度 | 适用场景 | 推荐指数 |
| :---- | :---- | :---- | :---- | :---- | :---- |
| **ReAct (V2.1 Engine B)** | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 中 | 单点模糊判断 (如: 这段话是不是病史) | ✅ **局部使用** |
| **Planner** | ⭐⭐ | ⭐⭐⭐⭐ | 高 | 复杂任务规划 (如: 生成全项目总结) | ❌ **质控不用** |
| **SOP 状态机 (V2.4)** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 低 | **临床试验全流程控制** (SOP 落地) | 👑 **核心架构** |
## **6\. 深度释疑SOP 会导致僵化吗?(Flexibility Check)**
针对你担心的“每个字段都要配流程”和“浪费 LLM 能力”的问题,我们需要纠正一个认知误区。
### **6.1 误区SOP \= 细粒度代码逻辑**
* **错误做法**:把 400 个变量写成 400 个节点。节点1: 查年龄 \-\> 节点2: 查性别 \-\> 节点3: 查身高...
* **后果**:配置地狱,灵活性为零。
### **6.2 正解SOP \= 粗粒度业务阶段 (Phases)**
我们通过 **“粗颗粒度节点”** 来充分释放 LLM 的能力。一个 SOP 可能只有 3 个节点,但涵盖了无数逻辑。
* **节点 A (基线硬扫描)**
* **类型**硬规则Hard Rule
* **内容**:一次性加载 50 条 JSON Logic 规则。
* **执行**CPU 毫秒级跑完所有数值校验(年龄、身高、体重、化验值范围)。
* **配置**:这是一个列表,不是流程图,配置很简单。
* **节点 B (AI 综合研判)**
* **类型**软指令Soft Instruction / ReAct
* **Prompt**:“请作为医学专家,阅读患者的‘现病史’、‘既往史’和‘手术史’。请综合判断是否存在:严重心血管疾病、未控制的高血压或活动性感染。如果有,请引用原文说明。”
* **LLM 的灵活性**
* 在这个节点**内部**LLM 是完全自由的。
* 它可以自己决定先看现病史还是既往史。
* 它可以自己判断“高血压二级”算不算“未控制”。
* **它依然在用 ReAct 调用工具**,只是我们把它限制在了“检查病史”这个大阶段里,不让它跑去“检查知情同意书”。
### **6.3 实战推演:如何拆解一个 400 变量的项目?**
假设你的项目有 10 个表400 个变量。
| 数据类型 | 变量数量 | 举例 | 处理策略 | 节点归属 |
| :---- | :---- | :---- | :---- | :---- |
| **人口学/基线** | 20 个 | 年龄、性别、身高、体重 | **Hard Rule** | **节点 A (基线检查)** |
| **实验室检查** | 300 个 | 血常规、生化、尿常规 | **Hard Rule** | **节点 A (基线检查)** |
| **体格检查** | 50 个 | 心率、血压、体温 | **Hard Rule** | **节点 A (基线检查)** |
| **既往病史** | 5 个 | 自由文本描述 | **Soft Instruction** | **节点 B (病史研判)** |
| **合并用药** | 5 个 | 自由文本药名 | **Soft Instruction** | **节点 C (用药研判)** |
| **不良事件** | 20 个 | 严重程度、转归情况 | **Soft Instruction** | **节点 D (安全监控)** |
**结论**
* 尽管有 400 个变量,**SOP 流程图里其实只有 4 个节点**。
* 你只需要写 **3 条** AI 指令(针对节点 B, C, D
* 剩下的 370 个变量,全部用简单的 JSON 规则批量处理。
**这不仅没有浪费 LLM 的能力,反而让它更专注(只处理那 30 个最难的变量),并帮你省下了巨额的 Token 费用。**
## **7\. 给 2 人团队的行动指南**
1. **宏观上用 SOP**
* 临床研究最讲究 SOP。用 JSON 定义好“先查A再查B最后查C”的流程。这能保证你的系统像瑞士钟表一样稳定。
* 这解决了“不知道 AI 会不会乱跑”的恐惧。
2. **微观上用 ReAct**
* 在 SOP 的某个具体节点(比如“检查病史”节点)里,允许 AI 使用 ReAct 模式(思考-查库-判断)。
* 这保留了处理非结构化数据的灵活性。
3. **实现路径**
* Phase 2.1: 先把之前的 hard\_rules 列表改成简单的 nodes 结构(线性执行)。
* Phase 2.2: 在 nodes 里增加 on\_pass / on\_fail 跳转逻辑。
**一句话总结:用 SOP (状态机) 管住 Agent 的腿(流程),用 ReAct 管住 Agent 的嘴(推理)。这就是既稳定又灵活的最佳实践。**

View File

@@ -0,0 +1,167 @@
# **IIT Manager Agent V2.6 智能化升级方案:双脑架构**
**背景:** 响应产品经理关于“系统不够智能、交互僵硬”的反馈。
**核心目标:** 在保留 V2.5 **SOP 严谨性**(左脑)的基础上,引入 **ReAct 灵活性**(右脑),打造“懂人话”的智能助手。
**适用对象:** 2人高效开发团队
## **1\. 核心理念:为什么我们需要“两个大脑”?**
我们之前的架构V2.5)构建了一个完美的\*\*“左脑”**(逻辑、规则、流程),现在我们需要补上**“右脑”\*\*(直觉、推理、对话)。
| 维度 | 左脑 (SOP 状态机) \- V2.5 | 右脑 (ReAct Agent) \- V2.6 新增 |
| :---- | :---- | :---- |
| **擅长** | 执行标准流程、合规检查、填报表 | 处理模糊提问、多步查询、综合分析 |
| **典型指令** | "对 P001 进行入排质控" | "帮我查下最近那个发烧的病人是谁?" |
| **思维方式** | **线性执行** (Step 1 \-\> Step 2\) | **循环推理** (思考 \-\> 查库 \-\> 发现不够 \-\> 再查) |
| **安全性** | 极高 (写操作必须走这里) | **只读 (Read-Only)**,严禁直接修改数据 |
| **用户评价** | "靠谱但死板" | "聪明但不可控" |
**解决方案:**
我们不是要推翻 V2.5,而是要在它旁边挂载一个 **ReAct Agent**,并通过一个智能的 **意图路由器 (Router)** 来决定用哪个脑子。
## **2\. 架构升级:双脑路由模型 (The Dual-Brain Architecture)**
我们在 ChatService 中引入一个由 LLM 驱动的**意图识别层**。
graph TD
User\[用户输入: "那个发烧的病人..."\] \--\> Router\[🧠 意图路由层 (LLM)\]
Router \--\>|意图: 模糊查询/分析| ReAct\[🎨 右脑: ReAct Agent\]
Router \--\>|意图: 标准作业/写操作| SOP\[📐 左脑: SOP Engine\]
Router \--\>|意图: 信息缺失| AskBack\[❓ 追问机制\]
subgraph "共享工具池 (ToolsService)"
T1\[read\_clinical\_data\]
T2\[search\_protocol\]
T3\[get\_project\_stats\]
end
ReAct \--\>|调用 (只读)| T1
ReAct \--\>|调用 (只读)| T2
SOP \--\>|调用 (读写)| T1
SOP \--\>|调用 (读写)| T3
## **3\. 关键组件实现 (Phase 5\)**
### **3.1 智能意图路由 (The Router)**
不要用正则表达式,直接用 LLM 判断用户想干什么。
// backend/src/modules/iit-manager/services/IntentService.ts
export class IntentService {
async detect(message: string, history: any\[\]): Promise\<IntentResult\> {
const prompt \= \`
你是一个临床研究助手的"分诊台"。请分析用户输入,返回 JSON。
用户输入: "${message}"
分类标准:
1\. QC\_TASK: 明确的质控、检查、录入指令(如"检查P001")。
2\. QA\_QUERY: 模糊的查询、分析、统计问题(如"查下那个发烧的...")。
3\. UNCLEAR: 指代不清,缺少关键信息(如"他怎么样了?")。
返回格式: { "type": "QC\_TASK" | "QA\_QUERY" | "UNCLEAR", "reason": "...", "missing\_info": "..." }
\`;
const result \= await this.llm.chat(prompt);
return JSON.parse(result);
}
}
### **3.2 右脑ReAct Agent (The Smart Loop)**
PM 提到的 ReAct 是对的。我们需要一个循环,让 AI 自己决定调什么工具,调几次。
**核心逻辑:**
1. **思考**:用户问“最近入组的女性患者平均年龄”,我需要先查最近入组名单,再查她们的年龄,最后计算。
2. **行动**:调用 read\_clinical\_data(filter="recent")。
3. **观察**:拿到 5 个患者数据。
4. **思考**:数据有了,我自己算一下平均值。
5. **回答**35.5岁。
// backend/src/modules/iit-manager/engines/ReActEngine.ts
export class ReActEngine {
constructor(private tools: ToolsService) {}
async run(query: string, context: any) {
let messages \= \[
{ role: 'system', content: \`你是一个智能助手。你可以使用工具回答问题。请使用 ReAct 模式思考。\` },
{ role: 'user', content: query }
\];
// 最多思考 5 轮,防止死循环费钱
for (let i \= 0; i \< 5; i++) {
const response \= await this.llm.chat(messages, { tools: this.tools.getDefinitions() });
// 1\. AI 决定结束
if (\!response.hasToolCall) return response.content;
// 2\. AI 决定调工具
const toolResult \= await this.tools.execute(response.toolName, response.args);
// 3\. 把结果喂回去
messages.push({ role: 'tool', content: JSON.stringify(toolResult) });
}
}
}
### **3.3 追问机制 (Clarification)**
这是解决“像个傻子”的关键。如果意图识别是 UNCLEAR不要报错要追问。
* **User**: "他怎么样了?"
* **Old Agent**: "未找到指令。" (傻子)
* **New Agent**: "请问您指的是哪位患者?是刚刚讨论的 P001 吗?" (智能)
**实现**
在 ChatService 中,如果 IntentService 返回 UNCLEAR直接把 missing\_info 包装成反问句返回给用户。
## **4\. 融合后的开发计划更新 (Merge into V2.5)**
我们将 PM 的建议整合进开发计划,作为 **Week 4+ 的核心任务**
### **Phase 5智能化增强 (Smart Layer) \- 新增**
| 时间 | 任务 | 说明 |
| :---- | :---- | :---- |
| **Day 22** | **意图识别路由** | 实现 IntentService替代 Keyword 匹配。 |
| **Day 23** | **ReAct 引擎实现** | 实现多轮思考循环,对接 ToolsService复用现有工具。 |
| **Day 24** | **上下文记忆增强** | 升级 SessionMemory支持由近及远的对话历史回溯。 |
| **Day 25** | **主动追问 UI** | 前端支持“建议气泡”Suggestion Chips方便用户点击回复追问。 |
## **5\. 风险控制:给 ReAct 戴上镣铐**
为了防止 PM 担心的“太灵活导致不可控”,我们需要加两条铁律:
1. **右脑ReAct只读不写**
* ReAct Agent 只能调用 read\_\* 和 search\_\* 类工具。
* 如果它想修改数据(如 update\_record必须引导用户“看来你需要修改数据这属于质控流程请确认是否启动【修改 SOP】
* **原因**:避免 AI 在聊天中随口把临床数据改了,这是合规红线。
2. **幻觉熔断**
* ReAct 循环中,如果连续 2 次调用工具报错,强制终止并转人工。
* 避免 AI 在那里自言自语浪费 Token。
## **6\. 结论**
**你是对的PM 也是对的。**
* **V2.5 (SOP)** 是我们的**骨架**,保证我们站得稳(合规、准确)。
* **PM 建议 (ReAct)** 是我们的**血肉**,让我们看起来像个人(灵活、智能)。
**最终策略:**
继续执行 V2.5 的前 3 周计划(那是基础)。
**Week 4**,原本计划做“视觉识别”,现在建议**替换为“智能化增强ReAct \+ 意图路由)”**。
* 理由:视觉识别是锦上添花,而“不被当成傻子”是用户留存的关键。
**行动:** 请批准将“视觉识别”延后,优先开发“双脑路由”模块。

View File

@@ -0,0 +1,118 @@
# **IIT Manager Agent 架构决策白皮书:极简主义的胜利**
**文档版本:** V1.0
**日期:** 2026-02-03
**面向对象:** 开发团队、架构师、项目相关人员
**核心议题:** 为什么我们放弃了 MCP 和 Skills选择了 Postgres \+ Service Class
## **1\. 我们的现状与挑战 (Context)**
在做出任何架构决策之前,必须诚实地面对我们当前的约束条件。
* **团队规模**2 人(精英小队,但人力极其有限)。
* **技术栈**Node.js (Fastify) \+ PostgreSQL (Prisma) \+ Postgres-Only (pg-boss)。
* **业务场景**
* **医疗垂直领域**对数据的准确性、安全性、合规性要求极高GCP/FDA
* **高并发写入**:未来 100+ 项目47 家中心同时录入 REDCap。
* **实时响应**PI 在企业微信提问,期望秒级回复。
* **核心痛点**:既要应对复杂的业务逻辑(不同项目的入排标准千差万别),又要保证系统的绝对稳定(医疗数据不能错),还要在有限的人力下快速交付。
## **2\. 为什么我们放弃了 "Skills" 和 "MCP Server"**
这两个概念在 AI Agent 领域非常火,但经过深度评估,对于现阶段的我们来说,它们是\*\*“屠龙之术”\*\*。
### **2.1 放弃 "Skills" (作为独立框架)**
通常所说的 Skills 往往指代复杂的 Agent 编排框架(如 LangChain 中的 Tools/Skills 概念),或者需要专门的解析器来处理的复杂 DSL领域特定语言
* **问题所在**
* **学习成本高**:团队需要学习一套新的框架或语法。
* **调试困难**:一旦 Agent 不按套路出牌,很难追踪是 Prompt 写错了,还是框架解析错了。
* **过度设计**:对于绝大多数医疗质控逻辑(如 age \> 18用自然语言让 LLM 去猜,不如直接写代码判断来得准确、高效、省钱。
* **我们的替代方案****Postgres JSON 配置**。
* 我们将“技能”降维为**数据**。
* hard\_rules用 JSON Logic 描述死规则,由 CPU 执行100% 准确。
* soft\_instructions用一段 Prompt 描述软逻辑,由 LLM 执行。
* **本质**:我们没有抛弃 Skills 的**理念**(将逻辑配置化),我们抛弃的是 Skills 的**复杂实现**。
### **2.2 放弃 "MCP Server" (Model Context Protocol)**
MCP 是 Anthropic 推出的一种标准协议,用于连接 AI 和数据源。
* **问题所在**
* **架构复杂度爆炸**:引入 MCP 意味着我们需要维护独立的 Server 进程、处理进程间通信 (IPC/RPC)、处理网络延迟和断连。
* **运维负担**:对于 2 人团队,多维护一个服务就是多一份心智负担。
* **牛刀杀鸡**:我们的 Agent 和 REDCap Adapter 都在同一个 Node.js 进程里,直接调用函数只要几纳秒;如果走 MCP 协议,需要序列化-\>发送-\>接收-\>反序列化,性能损耗巨大且毫无必要。
* **我们的替代方案****Service Class (伪 MCP)**。
* 我们在代码层面定义一个统一的 ToolsService 类。
* 它向外暴露标准的接口(就像 MCP 一样),但内部直接调用函数。
* **本质**:我们保留了 MCP 的**接口思想**(解耦、标准化),抛弃了 MCP 的**通信协议**。
## **3\. 我们的架构:极简主义的工程实践**
我们将这套架构命名为 **"Postgres-Only \+ Service-First \+ SOP State Machine"**。
### **3.1 架构全景图**
graph TD
subgraph "核心优势:全在内存里,全在库里"
DB\[(Postgres DB)\]
NodeJS\[Node.js 后端服务\]
DB \--\>|1. 加载配置 (Skills)| NodeJS
subgraph "Node.js 内部逻辑"
Engine\[QcEngineService\]
Engine \--\>|2. 执行| HardRule\[Engine A (CPU)\]
Engine \--\>|3. 思考| LLM\[Engine B (AI)\]
LLM \--\>|4. 调用| Tools\[ToolsService\]
HardRule \--\>|4. 调用| Tools
Tools \--\>|5. 执行| Adapter\[RedcapAdapter\]
end
Adapter \--\>|6. 请求| External((REDCap))
end
### **3.2 核心组件解析**
1. **Skills as Data (数据化技能)**
* **实现**iit\_skills 表中的 JSON 字段。
* **优势**:热更新。医生在后台改了配置,下一秒 Agent 就生效,无需重启服务。
2. **Service as Tool (服务即工具)**
* **实现**ToolsService 类。
* **优势**
* **极速**:函数调用,零延迟。
* **稳定**:利用 TypeScript 的类型检查,编译期就能发现错误。
* **可控**:我们可以轻松地在代码里加入重试、熔断、日志逻辑。
3. **SOP State Machine (SOP 状态机)**
* **实现**:通过 JSON 定义的流程图Check A \-\> Check B
* **优势****确定性**。医疗流程不能随机应变,必须严格合规。状态机保证了 Agent 永远在轨道上运行。
4. **Self-Correction Loop (自我修正回路)**
* **实现**:在 SoftRuleEngine 中捕获工具调用错误,反馈给 LLM 重试。
* **优势**:大大提升了系统的鲁棒性,减少了因为 LLM "手滑" 导致的报错。
## **4\. 为什么这套架构更适合我们?(SWOT 分析)**
| 维度 | Strengths (优势) | Weaknesses (劣势) | Opportunities (机会) | Threats (威胁) |
| :---- | :---- | :---- | :---- | :---- |
| **效率** | 开发极快,复用现有 Node.js 代码。 | 不支持跨语言调用(目前也不需要)。 | 快速迭代,最快速度上线 MVP。 | 随着业务极其复杂,单一 Service 类可能变大。 |
| **稳定性** | 链路极短,没有网络开销,没有新进程。 | 强耦合在 Node.js 生态内。 | 极低的出错率,适合医疗场景。 | 无。 |
| **成本** | **零新增成本**。不费服务器资源,不费 Token (Engine A)。 | 需自行维护 Service 代码。 | 将节省的 Token 成本转化为利润。 | 无。 |
| **扩展性** | 通过 JSON 配置即可支持新项目。 | 无法直接对接外部标准 MCP 工具。 | 形成行业壁垒:我们的 JSON 配置库就是核心资产。 | 如果未来 MCP 成为绝对主流,可能需要一层适配器。 |
## **5\. 写给开发团队的话**
兄弟们,我知道大家对新技术充满了热情,看到 "MCP"、"LangChain" 这样的词汇会觉得很兴奋。但在 IIT Manager 这个项目上,我们要**克制**。
1. **我们要的是“结果”,不是“概念”**。用户不在乎你用的是不是 MCP他们只在乎\*\*“我问的问题能不能秒回”****“我的数据质控准不准”\*\*。
2. **我们的护城河是“业务逻辑”,不是“胶水代码”**。把精力花在编写更精准的 iit\_skills 配置JSON Logic \+ Prompt这才是别人抄不走的壁垒。而 MCP 只是连接代码,谁都能写。
3. **简单就是力量**。2 个人维护一套 Node.js \+ Postgres 系统,我们可以睡个安稳觉。如果引入了复杂的微服务和协议,我们的大部分时间都会花在修修补补上。
**现在的架构,是我们在有限资源下,能做出的最优雅、最务实、最强大的选择。**
让我们基于这套 **V2.2 极简架构**,全速推进 Phase 2 的开发!

View File

@@ -0,0 +1,436 @@
# IIT Manager Agent 数据库设计
> **版本:** V2.9
> **更新日期:** 2026-02-05
> **关联文档:** [IIT Manager Agent V2.6 综合开发计划](./IIT%20Manager%20Agent%20V2.6%20综合开发计划.md)
>
> **V2.9 更新**
> - 扩展 `iit_skills` 表支持 Cron Skill主动提醒
> - 扩展 `iit_conversation_history` 表增加反馈字段
> - 更新 `iit_project_memory` 内容结构(用户画像)
---
## 1. 数据库表总览
| 表名 | 用途 | Phase | 优先级 |
|------|------|-------|--------|
| `iit_skills` | Skill 配置存储 | 1 | P0 |
| `iit_field_mapping` | 字段名映射字典 | 1 | P0 |
| `iit_task_run` | SOP 任务执行记录 | 2 | P0 |
| `iit_pending_actions` | 待处理的违规记录 | 2 | P0 |
| `iit_conversation_history` | 对话历史(流水账) | 2 | P1 |
| `iit_project_memory` | 项目级热记忆Markdown | 2 | P1 |
| `iit_weekly_reports` | 周报归档(历史书) | 4 | P1 |
| `iit_agent_trace` | ReAct 推理轨迹 | 5 | P2 |
| `iit_form_templates` | 表单模板(视觉识别) | 6 | 延后 |
---
## 2. Phase 1基础配置表
### 2.1 iit_skills - Skill 配置存储
```prisma
model IitSkill {
id String @id @default(uuid())
projectId String // 绑定项目
skillType String // qc_process | daily_briefing | general_chat | weekly_report | visit_reminder
name String // 技能名称
config Json // 核心配置 JSONSOP 流程图)
isActive Boolean @default(true)
version Int @default(1)
// V2.9 新增:主动触发能力
triggerType String @default("webhook") // 'webhook' | 'cron' | 'event'
cronSchedule String? // Cron 表达式,如 "0 9 * * *" (每天9点)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([projectId, skillType])
@@map("iit_skills")
@@schema("iit_schema")
}
```
**触发类型说明**
| triggerType | 触发方式 | 示例场景 |
|-------------|----------|----------|
| `webhook` | 用户消息触发(默认) | 质控任务、问答查询 |
| `cron` | 定时触发 | 访视提醒、周报生成 |
| `event` | 事件触发(预留) | AE 预警、数据变更通知 |
**Skill 配置示例SOP 流程图)**
```json
{
"name": "肺癌研究入组质控",
"start_node": "baseline_check",
"nodes": {
"baseline_check": {
"type": "hard_rule",
"rules": [
{ "field": "age", "logic": { ">=": [{"var":"age"}, 18] }, "message": "年龄<18岁" },
{ "field": "age", "logic": { "<=": [{"var":"age"}, 75] }, "message": "年龄>75岁" },
{ "field": "ecog", "logic": { "<=": [{"var":"ecog"}, 2] }, "message": "ECOG>2" }
],
"on_pass": "history_check",
"on_fail": "end_with_violation"
},
"history_check": {
"type": "soft_instruction",
"instruction": "检查既往史,排除间质性肺炎、活动性感染、严重心血管疾病。",
"tools": ["read_clinical_data"],
"on_pass": "medication_check",
"on_fail": "end_review_required"
},
"human_review": {
"type": "human_review",
"description": "需要 CRC 人工复核",
"on_approve": "end_success",
"on_reject": "end_rejected"
}
}
}
```
**Cron Skill 配置示例V2.9 新增)**
```json
{
"skillType": "visit_reminder",
"name": "每日访视提醒",
"triggerType": "cron",
"cronSchedule": "0 9 * * *",
"config": {
"start_node": "check_upcoming_visits",
"nodes": {
"check_upcoming_visits": {
"type": "soft_instruction",
"instruction": "查询未来 3 天内到期的访视,生成提醒列表",
"tools": ["read_clinical_data"],
"on_pass": "send_reminder",
"on_fail": "end_no_visits"
},
"send_reminder": {
"type": "soft_instruction",
"instruction": "根据用户画像选择合适的通知方式和语气,发送提醒",
"tools": ["send_message"],
"on_pass": "end_success"
}
}
}
}
```
---
### 2.2 iit_field_mapping - 字段名映射字典
```prisma
model IitFieldMapping {
id String @id @default(uuid())
projectId String // 项目级别映射
aliasName String // LLM 可能传的名称(如 "gender", "性别"
actualName String // REDCap 实际字段名(如 "sex"
createdAt DateTime @default(now())
@@unique([projectId, aliasName])
@@index([projectId])
@@map("iit_field_mapping")
@@schema("iit_schema")
}
```
**初始化示例**
```sql
INSERT INTO iit_field_mapping (project_id, alias_name, actual_name) VALUES
('project-uuid', 'age', 'age_calculated'),
('project-uuid', '年龄', 'age_calculated'),
('project-uuid', 'ecog', 'ecog_score'),
('project-uuid', '既往史', 'medical_history_text'),
('project-uuid', 'history', 'medical_history_text'),
('project-uuid', '性别', 'sex'),
('project-uuid', 'gender', 'sex');
```
---
## 3. Phase 2SOP 执行与记忆表
### 3.1 iit_task_run - SOP 任务执行记录
```prisma
model IitTaskRun {
id String @id @default(uuid())
projectId String
skillId String // 关联的 Skill
recordId String? // 关联的患者(如有)
triggeredBy String // 触发者 userId
status String // RUNNING | SUSPENDED | COMPLETED | FAILED
currentNode String? // 当前执行到的节点
trace Json? // 执行轨迹
resumeCallback String? // SUSPENDED 时,恢复后的下一步
rejectCallback String? // SUSPENDED 被拒绝时的下一步
suspendedAt DateTime?
resumedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([projectId, status])
@@index([recordId])
@@map("iit_task_run")
@@schema("iit_schema")
}
```
---
### 3.2 iit_pending_actions - 待处理的违规记录
```prisma
model IitPendingAction {
id String @id @default(uuid())
projectId String
taskRunId String // 来源任务
recordId String? // 关联患者
actionType String // violation | warning | review_required
field String? // 违规字段
message String // 违规描述
severity String // error | warning | info
resolvedBy String? // 处理人
resolvedAt DateTime?
resolution String? // 处理结论
createdAt DateTime @default(now())
@@index([projectId, actionType])
@@index([recordId])
@@map("iit_pending_actions")
@@schema("iit_schema")
}
```
---
### 3.3 iit_conversation_history - 对话历史(流水账)
> **V2.8 设计**:这是原始对话流水,不直接注入 Prompt只用于生成周报
>
> **V2.9 新增**:增加反馈字段,支持用户点赞/点踩
```prisma
model IitConversationHistory {
id String @id @default(uuid())
projectId String
userId String
recordId String? // 关联的患者(如有)
role String // user | assistant
content String @db.Text
intent String? // 识别出的意图类型
entities Json? // 提取的实体 { record_id, visit, ... }
// V2.9 新增:反馈循环
feedback String? // 'thumbs_up' | 'thumbs_down' | null
feedbackReason String? // 点踩原因:'too_long' | 'inaccurate' | 'unclear'
createdAt DateTime @default(now())
@@index([projectId, userId])
@@index([projectId, recordId])
@@index([createdAt])
@@map("iit_conversation_history")
@@schema("iit_schema")
}
```
---
### 3.4 iit_project_memory - 项目级热记忆
> **V2.8 核心表**:存储 Markdown 格式的热记忆,每次对话都注入 System Prompt
>
> **V2.9 扩展**:增加用户画像结构
```prisma
model IitProjectMemory {
id String @id @default(uuid())
projectId String @unique
content String @db.Text // Markdown 格式的热记忆
lastUpdatedBy String // 'system_daily_job' | 'admin_user_id' | 'profiler_job'
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("iit_project_memory")
@@schema("iit_schema")
}
```
**内容示例V2.9 增强版)**
```markdown
# 用户画像 (User Profiles)
## 张医生 (user_id: zhangyi)
- **角色**: PI
- **偏好**: 简洁汇报,只看结论,不要废话
- **关注点**: AE、入组进度
- **最佳通知时间**: 08:30
- **禁令**: 回复不超过 100 字
- **反馈统计**: 👍 12 / 👎 1
## 王护士 (user_id: wanghushi)
- **角色**: CRC
- **偏好**: 详细步骤,需要解释
- **关注点**: 访视安排、数据录入
- **最佳通知时间**: 09:00
# 当前状态 (Active Context)
- 当前阶段:入组冲刺期
- 重点关注P005 患者依从性差,需每日提醒
- 本周目标:完成 3 例入组
- P003 SAE 已判定为"可能无关"2月5日 PI 决策)
# 常见问题 (FAQ)
- 访视窗口V1 Day 1±3, V2 Day 28±7
# 系统禁令 (Rules)
- 严禁在未授权情况下删除数据
- 写操作必须经过人工确认
```
---
## 4. Phase 4周报归档
### 4.1 iit_weekly_reports - 周报归档(历史书)
> **V2.8 核心表**:存储每周的关键决策、进度、踩坑记录
```prisma
model IitWeeklyReport {
id String @id @default(uuid())
projectId String
weekNumber Int // 第几周(从项目开始计算)
weekStart DateTime // 周起始日期
weekEnd DateTime // 周结束日期
summary String @db.Text // Markdown 格式的周报内容
metrics Json? // 结构化指标 { enrolled: 3, queries: 5, ... }
createdBy String // 'system_scheduler' | 'admin_user_id'
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([projectId, weekNumber])
@@index([projectId])
@@map("iit_weekly_reports")
@@schema("iit_schema")
}
```
**内容示例**
```markdown
## Week 5 (2026-02-03 ~ 2026-02-09)
### 进度
- 本周新入组3 例
- 累计入组15 / 30 例
- 完成率50%
### 关键决策
- 2026-02-05: PI 会议决定放宽入排标准,允许 ECOG 3 分患者入组
- 2026-02-07: 确认 P003 AE 与研究药物"可能无关"
### 踩坑记录
- 曾尝试自动录入化验单,因 OCR 精度不足失败,已回退为人工复核
### 待办
- 清理 1 月份遗留的 Query
- 准备 2 月底期中分析数据
```
---
## 5. Phase 5ReAct 追踪表
### 5.1 iit_agent_trace - ReAct 推理轨迹
> 用于调试,不发送给用户,仅在 Admin 后台查看
```prisma
model IitAgentTrace {
id String @id @default(uuid())
projectId String
userId String
query String @db.Text // 用户原始问题
intentType String? // 识别的意图类型
trace Json // ReAct 的完整思考过程
tokenUsage Int? // 消耗的 Token 数
duration Int? // 执行时长ms
success Boolean
createdAt DateTime @default(now())
@@index([projectId, createdAt])
@@index([userId])
@@map("iit_agent_trace")
@@schema("iit_schema")
}
```
---
## 6. Phase 6视觉能力表延后到 V3.0
### 6.1 iit_form_templates - 表单模板
```prisma
model IitFormTemplate {
id String @id @default(uuid())
projectId String
formName String // REDCap 表单名称
fieldSchema Json // 表单字段结构
keywords String[] // 用于匹配的关键词
createdAt DateTime @default(now())
@@map("iit_form_templates")
@@schema("iit_schema")
}
```
---
## 7. 数据库迁移命令
```bash
# 生成迁移
npx prisma db push
# 生成客户端
npx prisma generate
# 查看表结构
npx prisma studio
```
---
## 8. 索引设计总结
| 表 | 索引 | 用途 |
|----|------|------|
| `iit_skills` | `[projectId, skillType]` (unique) | 按项目查询 Skill |
| `iit_field_mapping` | `[projectId, aliasName]` (unique) | 字段映射查询 |
| `iit_task_run` | `[projectId, status]` | 查询运行中/挂起的任务 |
| `iit_conversation_history` | `[projectId, userId]` | 按用户查询对话 |
| `iit_conversation_history` | `[projectId, recordId]` | 按患者查询对话 |
| `iit_conversation_history` | `[createdAt]` | 按时间范围查询 |
| `iit_weekly_reports` | `[projectId, weekNumber]` (unique) | 按周查询报告 |
| `iit_agent_trace` | `[projectId, createdAt]` | 按时间查询调试日志 |
---
**文档维护人**AI Agent
**最后更新**2026-02-05

View File

@@ -0,0 +1,731 @@
# IIT Manager Agent 核心引擎实现指南
> **版本:** V2.9
> **更新日期:** 2026-02-05
> **关联文档:** [IIT Manager Agent V2.6 综合开发计划](./IIT%20Manager%20Agent%20V2.6%20综合开发计划.md)
>
> **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 完整实现
```typescript
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 常用表达式
```javascript
// 范围检查
{ "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 完整实现
```typescript
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 类型定义
```typescript
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 完整实现
```typescript
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 安全约束
```typescript
// 工具白名单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 完整实现
```typescript
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. 引擎集成示意
```typescript
// 初始化
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

View File

@@ -0,0 +1,552 @@
# IIT Manager Agent 记忆系统实现指南 (V2.8)
> **版本:** V2.8
> **更新日期:** 2026-02-05
> **关联文档:** [IIT Manager Agent V2.6 综合开发计划](./IIT%20Manager%20Agent%20V2.6%20综合开发计划.md)
---
## 1. 记忆架构概览
### 1.1 设计原则
临床研究项目周期长达 1-3 年,需要"项目级长期记忆"来跨会话保持上下文。V2.8 架构采用**三层记忆体系**
| 层级 | 名称 | 存储位置 | 生命周期 | 检索方式 |
|------|------|----------|----------|----------|
| L1 | 流水账 | conversation_history | 30天 | 按需向量检索 |
| L2 | 热记忆 | project_memory | 持久 | 每次注入 |
| L3 | 历史书 | weekly_reports | 持久 | 按意图检索 |
> **注意**:用户偏好和患者关键信息统一存储在 `project_memory` 的 Markdown 中,无需单独表。
### 1.2 架构图
```
┌─────────────────────────────────────────────────────────────────────┐
│ MemoryService │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────────────┐ │
│ │ 流水账(L1) │ │ 热记忆(L2) │ │ 历史书(L3) │ │
│ │ conversation_ │ │ project_memory │ │ weekly_reports │ │
│ │ history │ │ │ │ │ │
│ │ │ │ - 项目元信息 │ │ - 周报卷叠 │ │
│ │ 30天自动过期 │ │ - 当前状态 │ │ - 关键决策归档 │ │
│ │ 向量化存储 │ │ - 关键决策 │ │ │ │
│ └────────────────┘ └────────────────┘ └────────────────────────┘ │
│ │ │ │ │
│ └───────────────────┴───────────────────────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ getContext │ │
│ │ ForPrompt() │ │
│ └───────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 2. 数据模型
### 2.1 流水账 (conversation_history)
```prisma
model iit_conversation_history {
id String @id @default(cuid())
project_id String
user_id String
role String // 'user' | 'assistant'
content String
intent String? // 意图标记
embedding Unsupported("vector(1536)")?
created_at DateTime @default(now())
expires_at DateTime // 30天后过期
@@index([project_id, user_id, created_at])
@@index([project_id, expires_at])
}
```
### 2.2 热记忆 (project_memory)
```prisma
model iit_project_memory {
id String @id @default(cuid())
project_id String
type String // 'meta' | 'status' | 'decision' | 'preference'
key String
value Json
priority Int @default(0) // 优先级,高的先注入
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@unique([project_id, type, key])
@@index([project_id, type])
}
```
### 2.3 历史书 - 周报 (weekly_reports)
```prisma
model iit_weekly_reports {
id String @id @default(cuid())
project_id String
week_start DateTime
week_end DateTime
summary String // 周报摘要Markdown
key_events Json // 关键事件 JSON
metrics Json // 统计指标
embedding Unsupported("vector(1536)")?
created_at DateTime @default(now())
@@unique([project_id, week_start])
@@index([project_id, week_start])
}
```
---
## 3. MemoryService 完整实现
> **文件路径**: `backend/src/modules/iit-manager/services/MemoryService.ts`
```typescript
import { prisma } from '../../common/prisma';
import { OpenAIEmbeddings } from '../../common/llm/embeddings';
import { LLMFactory } from '../../common/llm/adapters/LLMFactory';
export class MemoryService {
private embeddings: OpenAIEmbeddings;
private llm = LLMFactory.create('qwen');
constructor() {
this.embeddings = new OpenAIEmbeddings();
}
// ===== 1. 流水账操作 =====
async saveConversation(data: {
projectId: string;
userId: string;
role: 'user' | 'assistant';
content: string;
intent?: string;
}): Promise<void> {
const embedding = await this.embeddings.embed(data.content);
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 30);
await prisma.iit_conversation_history.create({
data: {
project_id: data.projectId,
user_id: data.userId,
role: data.role,
content: data.content,
intent: data.intent,
embedding: embedding,
expires_at: expiresAt
}
});
}
async searchConversations(
projectId: string,
query: string,
limit: number = 5
): Promise<ConversationResult[]> {
const queryEmbedding = await this.embeddings.embed(query);
const results = await prisma.$queryRaw<ConversationResult[]>`
SELECT id, content, role, intent, created_at,
1 - (embedding <=> ${queryEmbedding}::vector) as similarity
FROM iit_conversation_history
WHERE project_id = ${projectId}
AND expires_at > NOW()
ORDER BY embedding <=> ${queryEmbedding}::vector
LIMIT ${limit}
`;
return results;
}
// ===== 2. 热记忆操作 =====
async getHotMemory(projectId: string): Promise<HotMemory> {
const memories = await prisma.iit_project_memory.findMany({
where: { project_id: projectId },
orderBy: { priority: 'desc' }
});
return {
meta: memories.filter(m => m.type === 'meta'),
status: memories.filter(m => m.type === 'status'),
decisions: memories.filter(m => m.type === 'decision'),
preferences: memories.filter(m => m.type === 'preference')
};
}
async updateHotMemory(
projectId: string,
type: string,
key: string,
value: any,
priority: number = 0
): Promise<void> {
await prisma.iit_project_memory.upsert({
where: {
project_id_type_key: { project_id: projectId, type, key }
},
update: { value, priority },
create: {
project_id: projectId,
type,
key,
value,
priority
}
});
}
async refreshHotMemory(projectId: string): Promise<void> {
// 从最近的对话中提取关键信息更新热记忆
const recentConversations = await prisma.iit_conversation_history.findMany({
where: { project_id: projectId },
orderBy: { created_at: 'desc' },
take: 50
});
if (recentConversations.length === 0) return;
// 使用 LLM 提取关键信息
const prompt = `分析以下对话,提取需要长期记住的关键信息:
${recentConversations.map(c => `${c.role}: ${c.content}`).join('\n')}
请提取以下类别的信息JSON格式
1. status: 项目当前状态变化
2. decisions: 重要决策
3. preferences: 用户偏好
返回格式:
{
"status": [{"key": "...", "value": "...", "priority": 0-10}],
"decisions": [...],
"preferences": [...]
}`;
const response = await this.llm.chat([{ role: 'user', content: prompt }]);
try {
const extracted = JSON.parse(response.content);
for (const [type, items] of Object.entries(extracted)) {
for (const item of items as any[]) {
await this.updateHotMemory(projectId, type, item.key, item.value, item.priority);
}
}
} catch (e) {
console.error('[MemoryService] 热记忆提取失败:', e);
}
}
// ===== 3. 历史书操作 =====
async saveWeeklyReport(projectId: string, report: string): Promise<void> {
const weekStart = this.getWeekStart();
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
const embedding = await this.embeddings.embed(report);
await prisma.iit_weekly_reports.upsert({
where: {
project_id_week_start: { project_id: projectId, week_start: weekStart }
},
update: {
summary: report,
embedding
},
create: {
project_id: projectId,
week_start: weekStart,
week_end: weekEnd,
summary: report,
key_events: {},
metrics: {},
embedding
}
});
}
async searchWeeklyReports(
projectId: string,
query: string,
limit: number = 3
): Promise<WeeklyReport[]> {
const queryEmbedding = await this.embeddings.embed(query);
return prisma.$queryRaw<WeeklyReport[]>`
SELECT id, week_start, week_end, summary,
1 - (embedding <=> ${queryEmbedding}::vector) as similarity
FROM iit_weekly_reports
WHERE project_id = ${projectId}
ORDER BY embedding <=> ${queryEmbedding}::vector
LIMIT ${limit}
`;
}
async rollupWeeklyMemory(projectId: string): Promise<void> {
const weekStart = this.getWeekStart();
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
// 获取本周对话
const conversations = await prisma.iit_conversation_history.findMany({
where: {
project_id: projectId,
created_at: { gte: weekStart, lte: weekEnd }
},
orderBy: { created_at: 'asc' }
});
if (conversations.length === 0) return;
// 使用 LLM 生成周报摘要
const prompt = `你是一个临床研究项目的记录员。请根据本周的对话记录,生成一份周报摘要。
对话记录:
${conversations.map(c => `[${c.created_at.toISOString()}] ${c.role}: ${c.content}`).join('\n')}
要求:
1. 提取关键事件和决策
2. 统计主要指标
3. 记录重要问题和解决方案
4. 简洁明了不超过500字`;
const response = await this.llm.chat([{ role: 'user', content: prompt }]);
await this.saveWeeklyReport(projectId, response.content);
}
// ===== 4. 意图驱动的上下文组装 =====
async getContextForPrompt(
projectId: string,
intent: IntentResult
): Promise<string> {
const parts: string[] = [];
// ⚠️ L2 热记忆:始终注入
const hotMemory = await this.getHotMemory(projectId);
parts.push(this.formatHotMemory(hotMemory));
// ⚠️ L3 历史书:按意图检索
if (intent.type === 'QA_QUERY') {
// 模糊查询需要历史上下文
const relatedReports = await this.searchWeeklyReports(projectId, intent.entities?.query || '', 2);
if (relatedReports.length > 0) {
parts.push('## 相关历史记录\n' + relatedReports.map(r => r.summary).join('\n---\n'));
}
}
// ⚠️ L1 流水账:仅按需检索(当历史书不足时)
if (parts.length < 3 && intent.type === 'QA_QUERY') {
const relatedConversations = await this.searchConversations(projectId, intent.entities?.query || '', 3);
if (relatedConversations.length > 0) {
parts.push('## 相关对话记录\n' + relatedConversations.map(c => `${c.role}: ${c.content}`).join('\n'));
}
}
return parts.join('\n\n');
}
private formatHotMemory(hotMemory: HotMemory): string {
const sections: string[] = [];
if (hotMemory.meta.length > 0) {
sections.push('## 项目信息\n' + hotMemory.meta.map(m => `- ${m.key}: ${JSON.stringify(m.value)}`).join('\n'));
}
if (hotMemory.status.length > 0) {
sections.push('## 当前状态\n' + hotMemory.status.map(s => `- ${s.key}: ${JSON.stringify(s.value)}`).join('\n'));
}
if (hotMemory.decisions.length > 0) {
sections.push('## 关键决策\n' + hotMemory.decisions.map(d => `- ${d.key}: ${JSON.stringify(d.value)}`).join('\n'));
}
return sections.join('\n\n');
}
private getWeekStart(): Date {
const now = new Date();
const weekStart = new Date(now);
weekStart.setDate(now.getDate() - now.getDay() + 1);
weekStart.setHours(0, 0, 0, 0);
return weekStart;
}
// ===== 5. 清理过期数据 =====
async cleanupExpiredData(): Promise<void> {
await prisma.iit_conversation_history.deleteMany({
where: { expires_at: { lt: new Date() } }
});
}
}
// ===== 类型定义 =====
interface HotMemory {
meta: Array<{ key: string; value: any }>;
status: Array<{ key: string; value: any }>;
decisions: Array<{ key: string; value: any }>;
preferences: Array<{ key: string; value: any }>;
}
interface ConversationResult {
id: string;
content: string;
role: string;
intent: string | null;
created_at: Date;
similarity: number;
}
interface WeeklyReport {
id: string;
week_start: Date;
week_end: Date;
summary: string;
similarity: number;
}
interface IntentResult {
type: string;
entities?: {
record_id?: string;
query?: string;
};
}
```
---
## 4. 记忆信息映射
### 4.1 信息类型与存储位置
| 信息类型 | 存储位置 | 更新机制 |
|----------|----------|----------|
| 项目名称、PI、入组目标 | project_memory (meta) | 初始化时写入 |
| 当前入组人数、进度百分比 | project_memory (status) | 每日定时更新 |
| 用户做出的关键决策 | project_memory (decision) | 对话中实时提取 |
| 用户偏好设置 | project_memory (preference) | 用户明确表达时 |
| 每次对话原文 | conversation_history | 对话实时写入 |
| 每周总结 | weekly_reports | 周一凌晨卷叠 |
| 患者特殊情况 | project_memory (当前状态) | 在热记忆中维护 |
### 4.2 检索策略
```
┌─────────────────────────────────────────────────────────────┐
│ 意图识别结果 │
└───────────────────────────┬─────────────────────────────────┘
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
QC_TASK QA_QUERY PROTOCOL_QA
│ │ │
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 热记忆 + SOP │ │热记忆 + 历史书│ │ 热记忆 + RAG │
│ 状态同步 │ │ + 流水账检索 │ │ 知识库检索 │
└──────────────┘ └──────────────┘ └──────────────┘
```
---
## 5. 性能优化建议
### 5.1 索引设计
```sql
-- 向量索引(用于相似度搜索)
CREATE INDEX idx_conversation_embedding
ON iit_conversation_history
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
CREATE INDEX idx_weekly_reports_embedding
ON iit_weekly_reports
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 50);
-- 复合索引(用于过滤)
CREATE INDEX idx_conversation_project_user_time
ON iit_conversation_history (project_id, user_id, created_at DESC);
```
### 5.2 缓存策略
```typescript
// 热记忆缓存(使用 Redis
class MemoryCache {
private redis: Redis;
async getHotMemory(projectId: string): Promise<HotMemory | null> {
const cached = await this.redis.get(`hot_memory:${projectId}`);
return cached ? JSON.parse(cached) : null;
}
async setHotMemory(projectId: string, memory: HotMemory): Promise<void> {
await this.redis.set(
`hot_memory:${projectId}`,
JSON.stringify(memory),
'EX', 300 // 5分钟缓存
);
}
}
```
### 5.3 Token 预算控制
```typescript
// 限制记忆上下文的 token 数量
async getContextForPrompt(projectId: string, intent: IntentResult): Promise<string> {
const MAX_TOKENS = 2000;
let context = '';
let tokenCount = 0;
// 优先注入热记忆
const hotMemory = await this.getHotMemory(projectId);
const hotMemoryStr = this.formatHotMemory(hotMemory);
tokenCount += this.estimateTokens(hotMemoryStr);
context += hotMemoryStr;
// 按优先级继续添加
if (tokenCount < MAX_TOKENS) {
// 添加历史书内容...
}
return context;
}
```
---
## 6. 验收标准
| 功能 | 验收标准 |
|------|----------|
| 流水账存储 | 对话消息 100ms 内写入成功 |
| 向量检索 | 相似度搜索 500ms 内返回 |
| 热记忆注入 | 每次请求正确注入项目上下文 |
| 周报卷叠 | 周一凌晨自动生成周报 |
| 过期清理 | 30天前的对话自动删除 |
---
**文档维护人**AI Agent
**最后更新**2026-02-05

View File

@@ -0,0 +1,281 @@
# IIT Manager Agent 开发阶段与任务清单
> **版本:** V2.9
> **更新日期:** 2026-02-05
> **关联文档:** [IIT Manager Agent V2.6 综合开发计划](./IIT%20Manager%20Agent%20V2.6%20综合开发计划.md)
>
> **V2.9 更新**
> - Phase 3 新增反馈循环任务
> - Phase 4 新增 ProfilerService 和 Cron Skill 任务
> - Phase 5 新增多意图处理任务
---
## 1. 开发阶段总览
```
Phase 1 Phase 2 Phase 3 Phase 4 Phase 5 Phase 6
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 基础工具层 │ ──▶ │ SOP 引擎 │ ──▶ │ ReAct 引擎 │ ──▶ │ 调度系统 │ ──▶ │ 智能路由 │ ──▶ │ 视觉能力 │
│ │ │ + 记忆L2 │ │ + 记忆L1 │ │ + 记忆L3 │ │ │ │ (延后) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
▼ ▼ ▼ ▼ ▼ ▼
ToolsService SopEngine ReActEngine SchedulerService IntentService VisionService
FieldMapping HotMemory FlowMemory WeeklyReports MixedRouting (Postponed)
HardRuleEngine SoftRuleEngine AgentTrace ReportService StreamingFB
```
---
## 2. Phase 1: 基础工具层
### 2.1 目标
- 搭建可复用的工具框架
- 实现字段映射机制
- 建立硬规则引擎
### 2.2 任务清单
| 任务ID | 任务名称 | 优先级 | 状态 | 前置依赖 |
|--------|----------|--------|------|----------|
| P1-01 | 创建 `iit_skills` 表 | 高 | 待开始 | - |
| P1-02 | 创建 `iit_field_mapping` 表 | 高 | 待开始 | - |
| P1-03 | 实现 `ToolsService` | 高 | 待开始 | P1-01 |
| P1-04 | 实现 `read_clinical_data` 工具 | 高 | 待开始 | P1-03 |
| P1-05 | 实现 `search_protocol` 工具 | 高 | 待开始 | P1-03 |
| P1-06 | 实现 `HardRuleEngine` | 高 | 待开始 | - |
| P1-07 | 集成字段映射到 ToolsService | 中 | 待开始 | P1-02, P1-03 |
| P1-08 | 单元测试覆盖 | 中 | 待开始 | P1-01~P1-07 |
### 2.3 验收标准
- [ ] 工具可通过名称调用
- [ ] 字段映射正确生效LLM 用 "年龄" → 实际调用 "dem_age"
- [ ] 硬规则拦截生效
- [ ] 测试覆盖率 > 80%
---
## 3. Phase 2: SOP 引擎 + 热记忆
### 3.1 目标
- 实现状态机驱动的 SOP 执行
- 搭建热记忆层L2
- 支持人工确认机制
### 3.2 任务清单
| 任务ID | 任务名称 | 优先级 | 状态 | 前置依赖 |
|--------|----------|--------|------|----------|
| P2-01 | 创建 `iit_task_run` 表 | 高 | 待开始 | - |
| P2-02 | 创建 `iit_pending_actions` 表 | 高 | 待开始 | - |
| P2-03 | 创建 `iit_project_memory` 表 | 高 | 待开始 | - |
| P2-04 | 实现 `SopEngine` 核心状态机 | 高 | 待开始 | P2-01 |
| P2-05 | 实现 `SoftRuleEngine` | 高 | 待开始 | - |
| P2-06 | 集成 SoftRuleEngine 到 SOP | 高 | 待开始 | P2-04, P2-05 |
| P2-07 | 实现 SUSPENDED 状态机制 | 高 | 待开始 | P2-04 |
| P2-08 | 实现人工确认流程 | 高 | 待开始 | P2-02, P2-07 |
| P2-09 | 实现 `MemoryService` 热记忆 | 中 | 待开始 | P2-03 |
| P2-10 | 集成热记忆到 SOP 上下文 | 中 | 待开始 | P2-04, P2-09 |
| P2-11 | 端到端测试:质控 SOP | 中 | 待开始 | P2-01~P2-10 |
### 3.3 验收标准
- [ ] 质控任务可自动执行完整 SOP
- [ ] 写操作正确等待人工确认
- [ ] SUSPENDED 状态正确持久化
- [ ] 热记忆正确注入 SOP 上下文
- [ ] 手动恢复执行成功
---
## 4. Phase 3: ReAct 引擎 + 流水账
### 4.1 目标
- 实现 ReAct 模式的灵活查询
- 搭建流水账层L1
- 实现只读安全约束
### 4.2 任务清单
| 任务ID | 任务名称 | 优先级 | 状态 | 前置依赖 |
|--------|----------|--------|------|----------|
| P3-01 | 创建 `iit_conversation_history` 表 | 高 | 待开始 | - |
| P3-02 | 创建 `iit_agent_trace` 表 | 中 | 待开始 | - |
| P3-03 | 实现 `ReActEngine` 核心循环 | 高 | 待开始 | P1-03 |
| P3-04 | 实现只读工具白名单 | 高 | 待开始 | P3-03 |
| P3-05 | 实现 `MemoryService` 流水账 | 高 | 待开始 | P3-01 |
| P3-06 | 实现向量化存储pgvector | 中 | 待开始 | P3-05 |
| P3-07 | 实现相似度检索 | 中 | 待开始 | P3-06 |
| P3-08 | 实现 Trace 记录机制 | 中 | 待开始 | P3-02, P3-03 |
| P3-09 | 实现流式反馈机制 | 高 | 待开始 | P3-03 |
| P3-10 | 实现 "正在思考" 状态提示 | 中 | 待开始 | P3-09 |
| P3-11 | 集成流水账到 ReAct 上下文 | 中 | 待开始 | P3-03, P3-05 |
| P3-12 | **[V2.9]** 扩展对话表支持反馈字段 | 中 | 待开始 | P3-01 |
| P3-13 | **[V2.9]** 实现反馈收集接口 | 中 | 待开始 | P3-12 |
| P3-14 | 端到端测试:模糊查询 | 中 | 待开始 | P3-01~P3-13 |
### 4.3 验收标准
- [ ] ReAct 可正确推理并调用工具
- [ ] 只读约束生效(无法调用写入工具)
- [ ] 流水账正确存储和检索
- [ ] Trace 记录可供调试
- [ ] 流式反馈 < 2秒首字节
- [ ] "正在思考" 状态正确显示
- [ ] **[V2.9]** 反馈按钮可正确收集用户反馈
---
## 5. Phase 4: 调度系统 + 历史书
### 5.1 目标
- 实现定时任务调度
- 实现周报生成
- 搭建历史书层L3
### 5.2 任务清单
| 任务ID | 任务名称 | 优先级 | 状态 | 前置依赖 |
|--------|----------|--------|------|----------|
| P4-01 | 创建 `iit_weekly_reports` 表 | 高 | 待开始 | - |
| P4-02 | 实现 `SchedulerService`pg-boss | 高 | 待开始 | - |
| P4-03 | 实现 `ReportService` | 高 | 待开始 | P4-01 |
| P4-04 | 实现周报自动生成 | 高 | 待开始 | P4-02, P4-03 |
| P4-05 | 实现记忆卷叠机制 | 中 | 待开始 | P4-01, P3-05 |
| P4-06 | 实现历史书检索 | 中 | 待开始 | P4-01 |
| P4-07 | 集成历史书到上下文组装 | 中 | 待开始 | P4-06 |
| P4-08 | **[V2.9]** 实现 `ProfilerService` | 中 | 待开始 | P2-03 |
| P4-09 | **[V2.9]** 扩展 Skill 表支持 Cron 触发 | 中 | 待开始 | P1-01 |
| P4-10 | **[V2.9]** 实现 Cron Skill 调度 | 中 | 待开始 | P4-02, P4-09 |
| P4-11 | **[V2.9]** 实现访视提醒 Skill | 中 | 待开始 | P4-10 |
| P4-12 | **[V2.9]** 集成用户画像到通知个性化 | 低 | 待开始 | P4-08, P4-11 |
| P4-13 | 端到端测试:周报生成 | 中 | 待开始 | P4-01~P4-12 |
### 5.3 验收标准
- [ ] 周报每周一自动生成
- [ ] 记忆卷叠每日自动执行
- [ ] 历史书检索正确召回
- [ ] **[V2.9]** 用户画像正确存储在 project_memory
- [ ] **[V2.9]** Cron Skill 按时触发
- [ ] **[V2.9]** 访视提醒正确发送给目标用户
---
## 6. Phase 5: 智能路由
### 6.1 目标
- 实现意图识别服务
- 实现混合路由(正则 + LLM
- 实现追问机制
### 6.2 任务清单
| 任务ID | 任务名称 | 优先级 | 状态 | 前置依赖 |
|--------|----------|--------|------|----------|
| P5-01 | 实现 `IntentService` | 高 | 待开始 | - |
| P5-02 | 实现正则快速通道 | 高 | 待开始 | P5-01 |
| P5-03 | 实现 LLM 意图识别 | 高 | 待开始 | P5-01 |
| P5-04 | 实现降级策略 | 中 | 待开始 | P5-01~P5-03 |
| P5-05 | 实现追问机制 | 中 | 待开始 | P5-01 |
| P5-06 | 扩展 `ChatService` 集成路由 | 高 | 待开始 | P5-01~P5-05 |
| P5-07 | **[V2.9]** 优化 ReAct Prompt 支持多意图 | 中 | 待开始 | P3-03 |
| P5-08 | 端到端测试:路由分发 | 中 | 待开始 | P5-01~P5-07 |
### 6.3 验收标准
- [ ] 简单指令 < 50ms 命中快速通道
- [ ] 复杂句子正确识别意图
- [ ] UNCLEAR 情况正确追问
- [ ] LLM 不可用时正确降级
- [ ] **[V2.9]** 多意图消息正确拆分并顺序执行
---
## 7. Phase 6: 视觉能力(延后)
> ⚠️ **注意**:根据风险评估,视觉能力延后到核心功能稳定后再开发。
### 7.1 目标
- 实现图片识别能力
- 支持知情同意书识别
- 支持 CRF 扫描件识别
### 7.2 任务清单
| 任务ID | 任务名称 | 优先级 | 状态 | 前置依赖 |
|--------|----------|--------|------|----------|
| P6-01 | 评估 GPT-4V / 通义千问-VL | 低 | 延后 | P1~P5 完成 |
| P6-02 | 实现 `VisionService` | 低 | 延后 | P6-01 |
| P6-03 | 集成到 ChatService | 低 | 延后 | P6-02 |
| P6-04 | 端到端测试 | 低 | 延后 | P6-03 |
### 7.3 延后原因
1. 核心功能优先级更高
2. 视觉能力成本较高
3. 需要更多真实场景验证
---
## 8. 里程碑与依赖关系
```mermaid
gantt
title IIT Manager Agent 开发里程碑
dateFormat YYYY-MM-DD
section Phase 1
基础工具层 :p1, 2026-02-10, 14d
section Phase 2
SOP 引擎 + 热记忆 :p2, after p1, 21d
section Phase 3
ReAct 引擎 + 流水账 :p3, after p2, 14d
section Phase 4
调度系统 + 历史书 :p4, after p3, 14d
section Phase 5
智能路由 :p5, after p4, 7d
section Phase 6
视觉能力 :p6, after p5, 14d
```
---
## 9. 风险与对策
| 风险 | 影响 | 对策 | 已整合 |
|------|------|------|--------|
| ReAct 决策链过长 | 延迟 > 10秒 | 流式反馈 + "正在思考" 状态 | ✅ |
| 混合意图难分类 | 用户困惑 | UNCLEAR + 追问机制 | ✅ |
| ReAct 误调写入工具 | 数据风险 | 只读工具白名单 | ✅ |
| UI 无响应感 | 体验差 | 流式反馈 + 状态提示 | ✅ |
| SOP 中途被打断 | 任务丢失 | SUSPENDED 状态 + 恢复机制 | ✅ |
| 视觉能力分散精力 | 核心功能延迟 | 延后到 Phase 6 | ✅ |
| **[V2.9]** 用户多意图混乱 | 任务遗漏 | ReAct Prompt 多意图拆分 | ✅ |
| **[V2.9]** 回复不符用户偏好 | 体验差 | 反馈循环 + 用户画像 | ✅ |
| **[V2.9]** 主动提醒打扰用户 | 用户投诉 | 最佳通知时间 + 个性化 | ✅ |
---
## 10. 性能指标
| 指标 | 目标值 | 测量方法 |
|------|--------|----------|
| 快速通道响应 | < 50ms | 正则匹配耗时 |
| LLM 意图识别 | < 1s | API 调用耗时 |
| SOP 单步执行 | < 2s | 包含工具调用 |
| ReAct 完整推理 | < 10s | 最多 5 轮循环 |
| 流式首字节 | < 2s | 第一个 token |
| 周报生成 | < 30s | 后台任务 |
| 向量检索 | < 500ms | Top-5 结果 |
---
**文档维护人**AI Agent
**最后更新**2026-02-05

View File

@@ -0,0 +1,327 @@
# IIT Manager Agent V2.6 综合开发计划
> **版本:** V2.9(极简架构 + SOP状态机 + 双脑路由 + 三层记忆 + 主动性增强)
> **日期:** 2026-02-05
> **团队规模:** 2人
> **预估周期:** 6周
> **核心目标:** 实现数据质控 Agent 的完整闭环 + 智能化交互 + 长期记忆 + 主动提醒 + 个性化
---
## 📚 文档导航
本开发计划已拆分为多个专项文档,便于查阅和维护:
| 文档 | 内容 | 适用场景 |
|------|------|----------|
| **本文档** | 架构总览、设计原则、验收标准 | 项目概览、立项汇报 |
| [01-数据库设计](./01-数据库设计.md) | Prisma 模型、索引设计、初始化 SQL | 数据库开发 |
| [02-核心引擎实现指南](./02-核心引擎实现指南.md) | HardRuleEngine、SoftRuleEngine、SopEngine、ReActEngine | 引擎开发 |
| [03-服务层实现指南](./03-服务层实现指南.md) | ToolsService、ChatService、IntentService 等 | 服务开发 |
| [04-记忆系统实现指南](./04-记忆系统实现指南.md) | V2.8 三层记忆架构、MemoryService | 记忆功能开发 |
| [05-开发阶段与任务清单](./05-开发阶段与任务清单.md) | Phase 1-6 详细任务、里程碑、验收标准 | 项目管理、进度跟踪 |
---
## 0. 架构适配性评估
本架构设计充分考虑了临床研究的多种业务场景,通过 **SOP状态机 + 双引擎 + 可扩展工具层** 的设计,实现了良好的适配性和扩展性。
### 0.1 目标场景覆盖度
| 场景 | 覆盖度 | 核心支撑组件 | 备注 |
|------|--------|-------------|------|
| **1. 拍照识别 + 自动录入** | 🟡 60% | VisionService + ToolsService | 延后到 V3.0 |
| **2. 数据质控** | 🟢 95% | HardRuleEngine + SoftRuleEngine | 核心场景 |
| **3. 入排标准判断** | 🟢 90% | SopEngine + 硬规则配置 | 配置 Skill 即可 |
| **4. 方案偏离检测** | 🟢 80% | SoftRuleEngine + search_protocol | 需配置访视窗口规则 |
| **5. AE事件检测** | 🟢 80% | 硬规则触发 + 软指令评估 | 需配置AE识别规则 |
| **6. 伦理合规检测** | 🟢 80% | HardRuleEngine | 配置伦理规则即可 |
| **7. 定期报告生成** | 🟡 50% | SchedulerService + ReportService | Phase 4 实现 |
### 0.2 架构扩展性评价
| 扩展维度 | 实现方式 | 复杂度 |
|----------|----------|--------|
| **新增质控规则** | 在 `iit_skills` 表插入 JSON 配置 | ⭐ 低 |
| **新增业务场景** | 新增 Skill 类型 + 配置 SOP 流程 | ⭐⭐ 中低 |
| **新增工具能力** | 在 ToolsService 增加工具定义 | ⭐⭐ 中低 |
| **新增数据源** | 新增 Adapter如 OdmAdapter | ⭐⭐⭐ 中 |
---
## 1. 架构决策总结
本计划基于以下架构设计文档的综合审查:
| 文档 | 核心观点 | 状态 |
|------|----------|------|
| **架构决策白皮书** | Postgres-Only + Service-First | ✅ 认可 |
| **V2.2 实施指南** | 混合双引擎(硬规则 + 软指令) | ✅ 认可 |
| **V2.2 工具泛化** | 3-5 个通用工具替代 100 个专用工具 | ✅ 认可 |
| **V2.3 健壮性设计** | 三层防御(映射 + 重试 + 兜底) | ✅ 认可 |
| **V2.4 SOP状态机** | 粗粒度 SOP 节点 + 节点内 ReAct | ✅ 认可 |
| **V2.8 记忆系统** | 三层记忆(流水账 + 热记忆 + 历史书) | ✅ 认可 |
| **V2.9 主动性增强** | Cron Skill + 用户画像 + 反馈循环 | ✅ 认可 |
### 1.0 V2.9 核心增强(新)
> **目标**:让 Agent 从"被动应答"进化为"主动协作",同时根据用户反馈持续优化
```
┌─────────────────────────────────────────────────────────────────────┐
│ V2.9 新增能力 │
├──────────────────┬──────────────────┬───────────────────────────────┤
│ 🔔 Cron Skill │ 👤 用户画像 │ 📊 反馈循环 │
│ 主动触发 SOP │ 个性化响应 │ 持续优化 │
│ 定时访视提醒 │ 最佳通知时间 │ 偏好自动调整 │
└──────────────────┴──────────────────┴───────────────────────────────┘
```
| 能力 | 实现方式 | 价值 |
|------|----------|------|
| **Cron Skill** | `iit_skills.triggerType = 'cron'` + pg-boss 调度 | 访视提醒、周报自动发送 |
| **用户画像** | `project_memory` 中存储用户偏好 Markdown | 回复风格个性化 |
| **反馈循环** | `conversation_history.feedback` 字段 | 持续改进回复质量 |
| **多意图处理** | ReAct Prompt 优化 + Chain of Thought | 一句话多任务 |
### 1.1 双脑路由模型
> **核心理念**:左脑(SOP) 保证严谨合规,右脑(ReAct) 提供灵活智能
```
┌─────────────────────────────────────────────────────────────────────┐
│ 企业微信 / 前端入口 │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 🧠 意图路由层 (IntentService) │
│ 混合路由:正则快速通道 + LLM 后备 │
└─────────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 📐 左脑 (SOP) │ │ 🎨 右脑 (ReAct) │ │ ❓ 追问机制 │
│ 结构化任务 │ │ 开放性查询 │ │ 信息不全 │
│ 写操作必经 │ │ 只读不写 │ │ 主动澄清 │
└──────────────────┘ └──────────────────┘ └──────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ ToolsService (工具层) │
│ 🔓 只读工具 (ReAct 可用) │ 🔒 读写工具 (仅 SOP 可用) │
└─────────────────────────────────────────────────────────────────────┘
```
**双脑对比**
| 维度 | 左脑 (SOP 状态机) | 右脑 (ReAct Agent) |
|------|-------------------|-------------------|
| **擅长** | 执行标准流程、合规检查 | 处理模糊提问、多步查询 |
| **典型指令** | "对 P001 进行入排质控" | "帮我查下最近那个发烧的病人" |
| **数据权限** | 读写皆可 | **只读 (Read-Only)** |
### 1.2 核心设计原则
| 原则 | 描述 |
|------|------|
| **Postgres-Only** | 无 Redis用 pg-boss 替代队列 |
| **Service-First** | 不用 MCP Server用 Service Class |
| **混合双引擎** | 硬规则(CPU) + 软指令(LLM) |
| **SOP 状态机** | 粗粒度节点 + 节点内灵活性 |
| **主动协作 (V2.9)** | Cron Skill 主动触发 + 用户画像个性化 |
| **反馈驱动 (V2.9)** | 用户反馈自动调整偏好 |
| **三层防御** | 字段映射 + 自我修正 + 空结果兜底 |
| **三层记忆** | 流水账(L1) + 热记忆(L2) + 历史书(L3) |
### 1.3 三层记忆架构 (V2.8)
> **关键洞察**:临床研究项目持续 1-3 年Agent 必须具备长期记忆能力
| 层级 | 名称 | 存储位置 | 生命周期 | 检索方式 |
|------|------|----------|----------|----------|
| L1 | 流水账 | conversation_history | 30天 | 按需向量检索 |
| L2 | 热记忆 | project_memory | 持久 | 每次注入 |
| L3 | 历史书 | weekly_reports | 持久 | 按意图检索 |
> 📖 详细实现请参阅 [04-记忆系统实现指南](./04-记忆系统实现指南.md)
---
## 2. 组件总览
### 2.1 已完成组件
| 组件 | 路径 | 状态 |
|------|------|------|
| ChatService | `services/ChatService.ts` | ✅ 需扩展 |
| SessionMemory | `agents/SessionMemory.ts` | ✅ 可复用 |
| RedcapAdapter | `adapters/RedcapAdapter.ts` | ✅ 可复用 |
| WechatService | `services/WechatService.ts` | ✅ 可复用 |
| DifyClient | `common/rag/DifyClient.ts` | ✅ 可复用 |
| LLMFactory | `common/llm/adapters/LLMFactory.ts` | ✅ 可复用 |
### 2.2 待开发组件
| 组件 | 优先级 | Phase | 详细文档 |
|------|--------|-------|----------|
| `iit_skills` 表 | P0 | 1 | [01-数据库设计](./01-数据库设计.md) |
| `iit_field_mapping` 表 | P0 | 1 | [01-数据库设计](./01-数据库设计.md) |
| `ToolsService` 类 | P0 | 1 | [03-服务层实现指南](./03-服务层实现指南.md) |
| `HardRuleEngine` 类 | P0 | 1 | [02-核心引擎实现指南](./02-核心引擎实现指南.md) |
| `SoftRuleEngine` 类 | P1 | 2 | [02-核心引擎实现指南](./02-核心引擎实现指南.md) |
| `SopEngine` 类 | P1 | 2 | [02-核心引擎实现指南](./02-核心引擎实现指南.md) |
| `MemoryService` 类 | P1 | 2-3 | [04-记忆系统实现指南](./04-记忆系统实现指南.md) |
| `ReActEngine` 类 | P1 | 3 | [02-核心引擎实现指南](./02-核心引擎实现指南.md) |
| `IntentService` 类 | P1 | 5 | [03-服务层实现指南](./03-服务层实现指南.md) |
| `SchedulerService` 类 | P2 | 4 | [03-服务层实现指南](./03-服务层实现指南.md) |
---
## 3. 开发阶段总览
> 📖 详细任务清单请参阅 [05-开发阶段与任务清单](./05-开发阶段与任务清单.md)
| Phase | 名称 | 周期 | 核心交付物 |
|-------|------|------|-----------|
| **Phase 1** | 基础工具层 | Week 1 | ToolsService, HardRuleEngine, 字段映射 |
| **Phase 2** | SOP 引擎 + 热记忆 | Week 2 | SopEngine, SoftRuleEngine, L2 热记忆 |
| **Phase 3** | ReAct 引擎 + 流水账 | Week 3 | ReActEngine, L1 流水账, 向量检索 |
| **Phase 4** | 调度系统 + 历史书 | Week 4 前半 | SchedulerService, ReportService, L3 历史书 |
| **Phase 5** | 智能路由 | Week 4 后半 - Week 5 | IntentService, 混合路由, 追问机制 |
| **Phase 6** | 视觉能力 | Week 6 (延后) | VisionService (延后到 V3.0) |
```
Phase 1 Phase 2 Phase 3 Phase 4 Phase 5 Phase 6
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ 基础工具 │ ──▶ │SOP引擎 │ ──▶ │ReAct │ ──▶ │ 调度 │ ──▶ │智能路由 │ ──▶ │视觉能力 │
│ 层 │ │+ 热记忆 │ │+ 流水账│ │+ 历史书│ │ │ │(延后) │
└────────┘ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘
```
---
## 4. 风险与应对
### 4.1 已采纳的风险对策
| 风险 | 已采纳对策 | 状态 |
|------|------------|------|
| **意图路由延迟感** | 混合路由(正则+LLM+ 流式反馈 | ✅ 已整合 |
| **ReAct 多嘴** | 只返回 Final AnswerTrace 存日志 | ✅ 已整合 |
| **SOP 状态机死锁** | SUSPENDED 挂起机制 + 唤醒机制 | ✅ 已整合 |
| **Phase 6 过早** | 延后到 V3.0 | ✅ 已标记 |
### 4.2 聚焦清单P0 优先级)
| 优先级 | 功能 | 理由 |
|--------|------|------|
| **P0** | ToolsService 健壮性 | 字段映射是地基 |
| **P0** | 每日早报 (Morning Brief) | 用户感知最强 |
| **P0** | 意图路由混合模式 | 保证响应速度 + 降低成本 |
### 4.3 主要风险矩阵
| 风险类别 | 风险 | 应对措施 |
|----------|------|----------|
| **基础架构** | LLM 响应慢 | 硬规则先行 + 混合路由 |
| **双脑架构** | ReAct 死循环 | 最大迭代 5 次 + Token 预算 |
| **双脑架构** | ReAct 调用写工具 | 工具白名单(只读) |
| **SOP 状态机** | 人工复核死锁 | SUSPENDED + 唤醒机制 |
| **扩展能力** | 视觉模型识别错误 | 低置信度人工确认 |
---
## 5. 成功标准
### 5.1 核心验收标准
| 阶段 | 验收标准 |
|------|----------|
| **Phase 1-3** | 质控任务 3秒内响应服务重启后任务可恢复对话历史持久化 |
| **Phase 4** | 周报每周一自动生成;定时任务连续 7 天无故障 |
| **Phase 5** | 意图识别准确率 > 85%;用户只收到 Final Answer |
| **Phase 6** | 图片识别准确率 > 85%(延后到 V3.0 |
### 5.2 性能指标
| 指标 | 目标值 |
|------|--------|
| 硬规则执行时间 | < 100ms |
| 软指令执行时间 | < 3s |
| 端到端响应时间 | < 5s |
| 正则快速通道延迟 | < 50ms |
| 意图识别时间LLM | < 1s |
| 向量检索延迟 | < 100ms |
| ReAct 平均迭代次数 | < 3 |
---
## 6. 文件路径清单
```
backend/src/modules/iit-manager/
├── services/
│ ├── ChatService.ts # 扩展:双脑路由 + 记忆集成
│ ├── IntentService.ts # 新建:意图识别 (Phase 5)
│ ├── ToolsService.ts # 新建:统一工具管理 (Phase 1)
│ ├── MemoryService.ts # 新建:三层记忆管理 (Phase 2-3)
│ ├── SchedulerService.ts # 新建:定时任务调度 (Phase 4)
│ └── ReportService.ts # 新建:报告生成 (Phase 4)
├── engines/
│ ├── HardRuleEngine.ts # 新建JSON Logic 执行器 (Phase 1)
│ ├── SoftRuleEngine.ts # 新建LLM 推理引擎 (Phase 2)
│ ├── SopEngine.ts # 新建:状态机调度器 (Phase 2)
│ └── ReActEngine.ts # 新建:多步推理引擎 (Phase 3)
├── agents/
│ └── SessionMemory.ts # 扩展:实体记忆 (Phase 5)
└── adapters/
└── RedcapAdapter.ts # 扩展writeRecord()
```
### 数据库新增表
> 📖 详细 Schema 请参阅 [01-数据库设计](./01-数据库设计.md)
| 表名 | 用途 | Phase |
|------|------|-------|
| `iit_skills` | Skill 配置存储 | 1 |
| `iit_field_mapping` | 字段名映射 | 1 |
| `iit_task_run` | SOP 任务执行记录 | 2 |
| `iit_pending_actions` | 待确认操作 | 2 |
| `iit_project_memory` | 热记忆L2 | 2 |
| `iit_conversation_history` | 流水账L1 | 2 |
| `iit_weekly_reports` | 历史书-周报L3 | 4 |
| `iit_agent_trace` | ReAct 推理轨迹 | 3 |
---
## 7. 参考文档
### 架构设计文档
1. [架构决策白皮书:极简主义的胜利](../00-系统设计/IIT%20Manager%20Agent%20架构决策白皮书极简主义的胜利.md)
2. [V2.2 落地实施指南](../00-系统设计/IIT%20Manager%20Agent%20V2.2%20落地实施指南.md)
3. [V2.3 健壮性设计与最佳实践](../00-系统设计/IIT%20Manager%20Agent%20V2.3.md)
4. [V2.4 SOP 状态机推荐](../00-系统设计/IIT%20Manager%20Agent%20V2.4.md)
5. [V2.6 双脑架构](../00-系统设计/IIT%20Manager%20Agent%20V2.6.md)
### 审核文档
1. [潜在的具体风险与问题](../05-测试文档/潜在的具体风险与问题.md)
2. [V2.8 记忆系统设计](../05-测试文档/IIT%20Manager%20Agent%20V2.8.md)
---
**文档维护人**AI Agent
**最后更新**2026-02-05
### 更新日志
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| V2.6 | 2026-02-02 | 整合双脑架构 + 三层记忆体系 |
| V2.6.1 | 2026-02-05 | 整合团队风险审查建议;拆分为多个专项文档 |
| V2.6.2 | 2026-02-05 | 简化表结构:删除 `iit_user_preferences``iit_patient_notes`(合并到 `project_memory` |
| V2.9 | 2026-02-05 | 主动性增强Cron Skill、用户画像、反馈循环、多意图处理 |

View File

@@ -0,0 +1,161 @@
# **IIT Manager Agent V2.7:基于“双层文本记忆”的极简架构**
**版本:** V2.7 (Clawbot Memory Edition)
**日期:** 2026-02-05
**核心变更:** 废弃复杂的 pgvector 语义检索,采用 Clawbot 式的“流水账 \+ 沉淀物”双层文本记忆。
**优势:** 记忆可读、可改、透明、零黑盒。
## **1\. 记忆架构重构 (Memory Refactoring)**
我们将记忆分为两层,完全模拟人类大脑运作,且摒弃不可解释的向量数据。
graph TD
subgraph "层级 1: 流水账 (Daily Stream)"
Log\[Conversation History\]
Note\[Raw Actions\]
desc1\[特点: Append-Only, 巨量, 易遗忘\]
end
subgraph "层级 2: 沉淀物 (The Sediment)"
ProjectMem\[项目级 MEMORY.md\]
PatientMem\[患者级 MEMORY.md\]
UserMem\[用户级 MEMORY.md\]
desc2\[特点: 精炼, 结构化, 人类可编辑\]
end
Log \--\>|每晚 Cron 归纳 (Gardening)| ProjectMem
Admin\[管理员/医生\] \--\>|手动修正/编辑| ProjectMem
ProjectMem \--\>|注入 System Prompt| LLM
## **2\. 数据库设计变更 (Schema Changes)**
### **2.1 新增 iit\_memory\_files 表**
我们不用磁盘文件,用数据库表来模拟文件,方便 Web 端管理。
model IitMemoryFile {
id String @id @default(uuid())
projectId String
targetType String // PROJECT | PATIENT | USER
targetId String // 对应的 ID (如 projectId, recordId, userId)
// 核心:这就是那个 MEMORY.md 的内容
content String @db.Text
lastUpdatedBy String // 'system\_daily\_job' 或 'user\_001'
updatedAt DateTime @updatedAt
@@unique(\[projectId, targetType, targetId\])
@@map("iit\_memory\_files")
@@schema("iit\_schema")
}
### **2.2 废弃计划**
* **废弃**iit\_conversation\_history 中的 embedding 字段。
* **废弃**Phase 5 中的向量检索服务。
## **3\. 核心服务实现MemoryService (V2.7)**
### **3.1 读取记忆 (Read)**
在 ChatService 启动对话前,简单的把文本读出来拼接到 Prompt 里。
// backend/src/modules/iit-manager/services/MemoryService.ts
export class MemoryService {
async getContext(projectId: string, userId: string, recordId?: string) {
// 1\. 获取项目级记忆 (规则、偏好)
const projectMem \= await this.loadMemory('PROJECT', projectId);
// 2\. 获取用户级记忆 (习惯)
const userMem \= await this.loadMemory('USER', userId);
// 3\. (可选) 获取患者级记忆
const patientMem \= recordId ? await this.loadMemory('PATIENT', recordId) : '';
return \`
\=== 长期记忆 (Long-Term Memory) \===
\[项目背景\]:
${projectMem}
\[用户偏好\]:
${userMem}
\[患者备注\]:
${patientMem}
\================================
\`;
}
private async loadMemory(type: string, id: string) {
const mem \= await prisma.iitMemoryFile.findUnique({
where: { projectId\_targetType\_targetId: { ... } }
});
return mem?.content || '';
}
}
### **3.2 记忆维护 (Gardening \- The Clawbot Way)**
这是最精彩的部分。我们不实时更新长期记忆(太乱),而是每天晚上让 AI 当“园丁”,修剪记忆。
**任务Daily Memory Consolidation**
* **触发**:每天凌晨 2 点。
* **输入**:当天的对话流水 (iit\_conversation\_history) \+ 旧的 MEMORY.md。
* **Prompt**"这是今天的对话流水。请提取其中新的重要事实(如用户偏好、新的关键决策、患者的新状态),合并到旧的记忆文件中。保持 Markdown 格式。如果没有新信息,保持不变。"
* **输出**:新的 Markdown 文本,覆盖数据库。
// SchedulerService.ts
async runDailyMemoryConsolidation() {
const logs \= await this.getDailyLogs();
const oldMem \= await this.memoryService.loadMemory('PROJECT', projectId);
const newMem \= await this.llm.chat(\[
{ role: 'system', content: '你是记忆整理员...' },
{ role: 'user', content: \`旧记忆:\\n${oldMem}\\n\\n今日流水:\\n${logs}\` }
\]);
await this.memoryService.saveMemory('PROJECT', projectId, newMem);
}
## **4\. 调整后的开发计划 (Phase 5 重构)**
我们将原计划 Week 5 的“向量检索”替换为“文本记忆系统”。
### **Week 5Clawbot 式记忆系统**
| 时间 | 任务 | 说明 |
| :---- | :---- | :---- |
| **Day 24** | **记忆表结构与 CRUD** | 创建 iit\_memory\_files 表,实现读写 API。 |
| **Day 25** | **记忆注入 Prompt** | 修改 ChatService在 System Prompt 头部注入 Markdown 内容。 |
| **Day 26** | **记忆整理 Worker** | 实现“每日记忆整理”的 Cron Job (pg-boss)。 |
| **Day 27** | **记忆编辑 UI** | 在 Admin 后台增加一个简单的文本框,允许人工修改 MEMORY.md。 |
## **5\. 为什么这比向量库好?**
1. **完全透明 (White Box)**
* 向量库出了问题(比如 AI 突然变傻),你只能重新 Embed甚至不知道哪条数据坏了。
* 文本记忆出了问题,你打开 Admin 界面,看到一行:“用户不喜欢红烧肉”,你把它删了,问题立刻解决。**这对医疗系统排查问题是无价的。**
2. **Token 可控**
* 向量库如果不加限制,可能召回 5000 字的无关内容。
* 文本记忆由“每日整理”压缩过通常只有几百字的核心干货Token 消耗极低。
3. **技术栈简化**
* 不需要 pgvector 插件(虽然你们装了,但不用也没关系)。
* 不需要 Embedding 模型调用。
* 纯字符串处理Node.js 最擅长。
## **6\. 结论**
这个建议**极具战略价值**。它把一个“高科技难题”(如何做语义检索)变成了一个“管理学问题”(如何整理笔记)。
**执行建议:**
全盘接受这个建议。在 V2.6 架构中,**移除 Embedding 层,替换为 Memory File 层。** 这会让你们的系统在面对 PI 的刁钻问题时,显得更有“记性”,同时让你们的运维工作变得无比轻松。

View File

@@ -0,0 +1,134 @@
# **IIT Manager Agent V2.8:基于“周报卷叠”的终极记忆架构**
**版本:** V2.8 (The Chronicle Edition)
**日期:** 2026-02-05
**核心变更:** 采纳“以周报代替向量检索”的建议。利用 LLM 的长窗口能力,通过周期性压缩数据,实现全生命周期的可读记忆。
**适用场景:** 1-3 年周期的临床研究项目。
## **1\. 核心理念:把“大数据”变成“厚书”**
我们不再试图在海量碎片数据中大海捞针Vector Search而是把数据写成一本\*\*“编年史”\*\*。
* **每日**:忠实记录流水账。
* **每周**LLM 阅读本周流水账,写一页“历史书”(周报)。
* **查询时**LLM 直接阅读整本“历史书”。
### **优势分析**
| 维度 | 向量检索 (V2.6/2.7) | 周报卷叠 (V2.8) |
| :---- | :---- | :---- |
| **准确性** | 模糊匹配,容易漏掉关键细节 | **全量阅读**,拥有上帝视角,极准 |
| **可解释性** | 黑盒向量,不知道 AI 看了啥 | **白盒周报**,医生可以随时翻阅、修正周报 |
| **技术难度** | 高 (Embedding, Vector DB) | **低** (Cron Job \+ LLM Summary) |
| **成本** | 检索便宜,但索引维护贵 | 生成周报耗 Token但查询极其高效 |
## **2\. 数据库设计 (Schema)**
### **2.1 新增 iit\_weekly\_reports 表**
model IitWeeklyReport {
id String @id @default(uuid())
projectId String
weekNumber Int // 例如: 202605 (2026年第5周)
startDate DateTime
endDate DateTime
// 核心LLM 生成的高浓缩总结
// 包含入组进度、发生的AE、关键沟通结论
summary String @db.Text
// 结构化数据 (可选,用于画图)
stats Json? // { "enrolled": 5, "queries": 2 }
createdAt DateTime @default(now())
@@unique(\[projectId, weekNumber\])
@@map("iit\_weekly\_reports")
@@schema("iit\_schema")
}
## **3\. 核心流程实现**
### **3.1 写入端:每周一凌晨的“编史官” (Cron Job)**
**SchedulerService.ts:**
// 每周一凌晨 02:00 执行
async generateWeeklyMemory(projectId: string) {
// 1\. 获取上周所有的原始对话 & 操作日志
const logs \= await this.rawLogs.get({
projectId,
from: lastMonday,
to: thisSunday
});
// 2\. 调用 LLM 进行“有损压缩”
const prompt \= \`
你是一个临床项目经理。请阅读上周的项目流水账,生成一份【周报记忆块】。
要求:
1\. 忽略闲聊和无关信息。
2\. 重点记录:入组人数变化、新增不良事件(AE)、主要方案偏离、PI的关键决策。
3\. 格式为 Markdown字数控制在 500 字以内。
流水账数据:
${JSON.stringify(logs)}
\`;
const summary \= await this.llm.chat(prompt);
// 3\. 存入数据库 (成为历史书的一页)
await prisma.iitWeeklyReport.create({
data: { projectId, summary, ... }
});
}
### **3.2 读取端:查询时的“速读” (Context Injection)**
当意图识别发现用户在问“过去”或“趋势”时,直接把**所有周报**读出来。
**ChatService.ts:**
async buildHistoryContext(projectId: string): Promise\<string\> {
// 1\. 取出该项目所有历史周报 (按时间正序)
// 3年也就 150 条Postgres 毫秒级返回
const reports \= await prisma.iitWeeklyReport.findMany({
where: { projectId },
orderBy: { weekNumber: 'asc' },
select: { weekNumber: true, summary: true }
});
// 2\. 拼接成一本“书”
// 格式:
// \[Week 2026-01\]: 入组 2 人,无异常。
// \[Week 2026-02\]: P003 发生 SAE已上报。
const chronicle \= reports.map(r \=\> \`\[Week ${r.weekNumber}\]: ${r.summary}\`).join('\\n');
return \`
\=== 项目编年史 (Project Chronicle) \===
${chronicle}
\====================================
\`;
}
## **4\. 最终开发计划修正 (Phase 5\)**
我们将 Week 5 的任务彻底简化:
| 时间 | 任务 | 说明 |
| :---- | :---- | :---- |
| **Day 24** | **原始日志表** | 确保 iit\_conversation\_history 记录完整。 |
| **Day 25** | **周报表结构** | 创建 iit\_weekly\_reports 表。 |
| **Day 26** | **编史官 Worker** | 写一个 Cron Job每周把 Log 压缩成 Report。 |
| **Day 27** | **记忆注入** | 在回答 QA\_QUERY 类问题时,将历史周报注入 Context。 |
## **5\. 总结:为什么这是终极方案?**
1. **解决了“上下文遗忘”**150 个周报拼接起来,刚好填满 DeepSeek 的上下文窗口。AI 可以看到**完整**的项目生命周期,这是 RAG 切片做不到的。
2. **解决了“幻觉”**:周报是持久化的,医生可以去后台检查某一周的周报写得对不对。如果 AI 瞎写,医生可以手动修正。**修正后的周报就是新的真理。**
3. **极简运维**:不需要维护向量库索引,不需要调优 Embedding 模型。就是简单的 SQL 查询和文本拼接。
**就用这个方案。这是目前为止最完美、最优雅的解决路径。**

View File

@@ -0,0 +1,83 @@
# **IIT Manager Agent V2.8:记忆信息映射指南**
**核心逻辑:** \> \* **即时/全局信息** ![][image1] 存入 **Hot Memory (Markdown)** ![][image1] 每次对话都带。
* **历史/阶段信息** ![][image1] 存入 **Weekly Reports (Database)** ![][image1] 查历史时全量读取。
## **1\. 信息存储位置映射表**
| 信息类型 | 存储位置 | 存储形式 | 举例 | 谁来维护? |
| :---- | :---- | :---- | :---- | :---- |
| **用户偏好** (User Preferences) | **Hot Memory** (Level 3\) | iit\_project\_memory 中的 Markdown 列表 | "PI 喜欢简报格式" "不要在周末发消息" | **人工编辑** (Admin) 或 **每日 AI 提炼** |
| **经常出现的上下文** (Frequent Context) | **Hot Memory** (Level 3\) | iit\_project\_memory 中的 Markdown 文本 | "当前处于入组冲刺阶段" "P001 是重点关注对象" | **每日 AI 提炼** (Cron Job) |
| **关键决策** (Key Decisions) | **Weekly Reports** (Level 2\) | iit\_weekly\_reports 中的 summary 字段 | "2026-02-01 决定放宽入排标准" "确认 P003 不良事件不相关" | **每周 AI 归档** (Scheduler) |
| **踩过的坑/经验教训** (Lessons Learned) | **双重存储** | **Hot** (如果是永久教训) **Weekly** (如果是历史记录) | **Hot**: "严禁直接调用 API 修改数据" **Weekly**: "W5 因参数错误导致同步失败" | **人工** (定规矩) **AI** (记流水) |
## **2\. 详细存储机制**
### **2.1 用户偏好 & 经常出现的上下文 \-\> Hot Memory (Markdown)**
这部分信息需要\*\*“时刻生效”\*\*,所以必须存成轻量级的 Markdown每次对话都注入 System Prompt。
* **存储表**iit\_project\_memory (config 字段)
* **内容示例**
\# User Preferences
\- \[PI\]: 汇报时只看数据,不要废话。
\- \[CRC\]: 下午 2 点后比较忙,尽量上午推任务。
\# Active Context
\- 当前重点任务:清理 3 月份的 Query。
\- 风险提示P005 患者依从性差,需每日提醒。
* **更新机制**
1. **被动更新**:管理员在后台手动修改。
2. **主动更新**每天凌晨AI 扫描昨日对话,如果发现用户说了“以后别...”,自动追加到这里。
### **2.2 关键决策 & 踩过的坑 \-\> Weekly Reports (编年史)**
这部分信息属于\*\*“项目历史”\*\*,不需要每次对话都挂在嘴边,但当用户问“回顾一下”时,需要能查到。
* **存储表**iit\_weekly\_reports
* **内容示例 (Week 2026-05)**
\[进度\]: 本周入组 3 人,累计 15 人。
\[决策\]: 2月3日 PI 会议决定:暂停筛选 "间质性肺炎" 既往史患者。
\[问题\]: 曾尝试自动录入化验单,但因 OCR 精度不足失败(踩坑记录),已回退为人工复核模式。
* **使用机制**
* 当用户问:“我们之前为什么暂停筛选肺炎患者?”
* 意图识别判断为 QUERY\_HISTORY。
* 系统一次性拉取过去所有周报 (150条记录),拼接成“编年史”,喂给 LLM 阅读并回答。
## **3\. 为什么这样设计?**
1. **偏好 (Preferences)** 必须是 **Hot** 的:
* 如果存到周报里AI 可能聊着聊着就忘了“老板不喜欢废话”这个规矩。只有放在 Hot Memory (System Prompt) 里,才能保证每一句回复都符合老板口味。
2. **决策 (Decisions)** 必须是 **Time-Series** 的:
* 决策往往有时间背景。存到周报里天然带有时间戳Week 5 做的决定)。这样 AI 才能回答“上个月做了什么决定”。
3. **踩坑 (Lessons)** 需要 **人工干预**
* 如果只是 AI 自动总结,可能会漏。
* V2.8 允许你在 Hot Memory 里手动写上一条:“【系统禁令】严禁在未授权情况下删除数据”。这条“人工植入的记忆”比任何 AI 总结都管用。
## **4\. 总结:信息流向图**
graph TD
User\[用户输入\] \--\>|对话流| Raw\[原始日志\]
subgraph "实时影响 (Hot)"
Raw \--\>|每日提炼| Prefs\[用户偏好\]
Raw \--\>|每日提炼| Context\[高频上下文\]
Admin\[人工干预\] \--\>|手动编辑| Prefs
end
subgraph "历史归档 (History)"
Raw \--\>|每周汇总| Decisions\[关键决策\]
Raw \--\>|每周汇总| Lessons\[踩坑记录\]
end
Prefs \--\>|注入| NextPrompt\[下一次对话\]
Context \--\>|注入| NextPrompt
Decisions \-.-\>|按需查询| NextPrompt
[image1]: <data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAXCAYAAADpwXTaAAAAX0lEQVR4XmNgGAWjYCQAeXn50+hiZAOgYU/QxcgGcnJy2kA8HV2cbAB03SwgDkIXB0lIkomnAfF1oBHM1DBsERCfRzGMHCCPy5ukAmgETEAXJwvIUzFpMMpTM9GOcAAAmV0cRTlI2MMAAAAASUVORK5CYII=>

View File

@@ -0,0 +1,143 @@
# **IIT Manager Agent V2.8:记忆检索与路由逻辑详解**
**核心机制:** 意图驱动的按需检索 (Intent-Driven Retrieval)
**目的:** 既保证 AI 懂当下Hot又保证 AI 懂历史History同时节省 Token。
## **1\. 记忆存储全景图 (Storage Map)**
在 V2.8 中,我们有三个核心存储位置,各司其职:
| 记忆类型 | 存储表 | 字段 | 内容特征 | 更新频率 |
| :---- | :---- | :---- | :---- | :---- |
| **1\. 流水账 (Raw Stream)** | iit\_conversation\_history | content (JSON) | 未经加工的原始对话。 | **实时** (每秒) |
| **2\. 热记忆 (Hot Context)** | iit\_project\_memory | config (Markdown) | 用户偏好、当前状态、系统禁令。 | **每日** (AI 提炼) |
| **3\. 历史书 (Weekly Reports)** | iit\_weekly\_reports | summary (Text) | 高度浓缩的周报、关键决策、踩坑记录。 | **每周** (AI 归档) |
## **2\. 调取顺序与逻辑 (Retrieval Logic)**
当 PI 发问时,系统并不是一股脑把所有记忆都塞进去,而是分三步走:
### **第一步:无条件注入 (Always On)**
**无论 PI 问什么,必须先注入“热记忆”。**
* **来源**iit\_project\_memory
* **内容**用户偏好如“PI 喜欢简报”)、当前项目状态。
* **理由**:这是 Agent 的“人设”和“底线”,一刻也不能忘。
### **第二步:意图识别 (Intent Detection)**
系统分析 PI 的问题,判断他想问“现在”还是“过去”。
* **情况 A问现在/执行任务** (如 "查下 P001", "生成本周周报")
* **动作****不调取** 历史书。
* **理由**:解决当下问题不需要翻阅 3 年前的老黄历,避免干扰 AI。
* **Context** \= Hot Memory \+ Current Task Data
* **情况 B问历史/趋势/回顾** (如 "回顾下去年的入组情况", "我们为什么暂停了筛选?")
* **动作****全量调取** 历史书 (iit\_weekly\_reports)。
* **理由**:需要上帝视角。
* **Context** \= Hot Memory \+ All Weekly Reports
### **第三步:上下文组装 (Prompt Assembly)**
最终发给 LLM 的 Prompt 是这样组装的:
\[System Prompt\]
你是一个临床研究助手...
\[Hot Memory\] (来自 iit\_project\_memory)
\- 用户偏好: 简洁、数据驱动
\- 当前状态: 入组阶段
\[History Context\] (仅在情况 B 下注入)
Week 1: 启动项目...
Week 2: 发现 P001 不良事件...
...
Week 50: 决定放宽标准...
\[User Question\]
我们之前为什么暂停筛选?
## **3\. 实战场景演示 (Scenario Walkthrough)**
### **场景 1PI 问 "P001 入组了吗?"**
1. **加载 Hot Memory**:获取偏好(简洁回复)。
2. **意图识别**QUERY\_DATA (查数据)。
3. **决策****不需要** 查历史书。
4. **行动**:调用 read\_clinical\_data 工具去 REDCap 查实时数据。
5. **回答**"已入组,时间是 2026-02-05。"
### **场景 2PI 问 "最近入组太慢了,我们之前有没有讨论过怎么解决?"**
1. **加载 Hot Memory**:获取偏好。
2. **意图识别**QUERY\_HISTORY (查历史决策)。
3. **决策****需要** 查历史书。
4. **行动**
* 从 iit\_weekly\_reports 拉取过去 20 周的 summary。
* 拼接成 3000 字的文本。
* 喂给 LLM。
5. **LLM 思考**:阅读周报,发现 Week 12 记录了“增加受试者交通补贴”的决策Week 15 记录了“在门诊增派 CRC”的决策。
6. **回答**"我们曾在第 12 周尝试增加补贴,第 15 周增派了 CRC当时效果有短暂提升..."
### **场景 3PI 问 "我上次跟你说的那个只要看结果的规矩,你记得吗?"**
1. **加载 Hot Memory**:获取偏好。
2. **AI 自检**:在 Hot Memory 里看到了 \- \[PI\]: 只要看结果 这条记录。
3. **回答**"记得的,教授。您要求汇报时只列数据结论,不要冗长的过程描述。我会严格遵守。"
* *注意:这个问题不需要查周报,也不需要查流水账,直接看 Hot Memory 就行。*
## **4\. 总结:给开发者的伪代码**
// ChatService.ts
async handleMessage(userId, message) {
// 1\. 总是加载热记忆 (Hot)
const hotMem \= await prisma.iitProjectMemory.findUnique(...);
// 2\. 意图识别
const intent \= await this.intentService.detect(message);
let historyContext \= "";
// 3\. 按需加载历史书 (History)
if (intent.type \=== 'QUERY\_HISTORY' || intent.type \=== 'ANALYZE\_TREND') {
const reports \= await prisma.iitWeeklyReport.findMany({
orderBy: { weekNumber: 'asc' }
});
historyContext \= reports.map(r \=\> \`\[W${r.weekNumber}\]: ${r.summary}\`).join('\\n');
}
// 4\. 组装最终 Prompt
const finalPrompt \= \`
${hotMem.content}
${historyContext}
User: ${message}
\`;
// 5\. 调用 LLM
return await this.llm.chat(finalPrompt);
}
**记住这个口诀:**
**“偏好时刻带,历史按需查,流水账只用来生成周报。”**
## **5\. 复杂度控制与防失控指南 (Safety Guardrails)**
为了防止系统变得“复杂不可控”,请严格遵守以下开发军规:
### **5.1 容量限制 (The Cap)**
* **Hot Memory**: 限制在 **2000 Tokens** 以内。如果超标,触发 MemoryPruningJob (记忆修剪任务),让 AI 自动总结或通知管理员手动删除。
* **History Book**: 每次加载不超过 **50 周** 的周报。如果项目运行了 10 年,只加载最近 1 年的周报,或者先让 AI 生成“年度总结”。
### **5.2 隔离原则 (Isolation)**
* **任务隔离**:执行 QC\_TASK (质控) 时,**严禁**加载 History Context。质控必须基于当下的事实不能受历史干扰。
* **数据隔离**LLM 永远没有权限直接读取 iit\_conversation\_history 表。这条物理隔绝保证了 Agent 永远不会被海量噪音淹没。
### **5.3 人工介入 (The Kill Switch)**
* **可读可改**:后台必须提供 Hot Memory 的编辑器。如果 Agent 记住了错误的规则(例如“不需要查年龄”),医生可以直接进去删掉那一行 Markdown。**这是系统失控时的“急停按钮”。**

View File

@@ -0,0 +1,210 @@
# **IIT Manager Agent V2.9 补充设计:主动性与用户画像增强**
**目标:** 响应智能化评估中的 P0 需求,补齐“主动服务”和“个性化”短板,并增强复杂任务处理能力。
**原则:** 坚持 **Postgres-Only**,复用现有的 **Skill (JSON)****ToolsService**
## **1\. 主动提醒机制设计 (The Proactive Engine)**
我们不需要写死一堆 cron 脚本,而是将“主动提醒”抽象为一种特殊的 **Skill**
### **1.1 架构复用逻辑**
* **触发源**pg-boss 的定时任务 (Scheduler)。
* **规则载体**iit\_skills 表(新增 trigger\_type: 'cron')。
* **执行者**QcEngineService (复用现有的逻辑)。
### **1.2 数据库 Schema 扩展**
在 iit\_skills 表中增加调度字段:
model IitSkill {
// ... 原有字段
triggerType String @default("webhook") // 'webhook' | 'cron' | 'event'
cronSchedule String? // 例如: "0 9 \* \* \*" (每天9点)
// 核心配置 (JSON)
config Json
}
### **1.3 Skill 配置示例:访视提醒**
这是一个“每天早上9点检查即将到期访视”的技能配置。
{
"name": "即将到期访视提醒",
"description": "扫描未来3天内需要访视的患者",
// 核心:复用 Engine A 的逻辑来筛选数据
"hard\_rules": \[
// 这里我们稍微扩展一下 hard\_rules 的能力,支持 SQL-like 的筛选
// 或者直接调用工具获取列表
\],
// 核心:复用 Engine B (LLM) 来生成人性化通知
"soft\_instructions": \[
{
"instruction": "请调用 \`get\_upcoming\_visits(days=3)\` 工具。如果有数据,请根据【用户偏好】生成一条提醒消息。如果没有数据,什么都不做。",
"tools": \["get\_upcoming\_visits", "send\_wechat\_msg"\]
}
\]
}
### **1.4 代码实现 (SchedulerService.ts)**
// SchedulerService.ts
async initCronJobs() {
// 1\. 从数据库加载所有 triggerType \= 'cron' 的 Skill
const cronSkills \= await prisma.iitSkill.findMany({
where: { triggerType: 'cron', isActive: true }
});
for (const skill of cronSkills) {
// 2\. 注册到 pg-boss
await this.boss.schedule(
\`run-skill-${skill.id}\`,
skill.cronSchedule,
{ skillId: skill.id }
);
}
}
// Worker 处理逻辑
async handleCronSkill(job) {
const { skillId } \= job.data;
const skill \= await prisma.iitSkill.findUnique({ where: { id: skillId } });
// 3\. 复用现有的引擎!
// 就像处理 Webhook 一样处理 Cron架构极其统一
await this.qcEngine.runSoftAgent(
skill.config.soft\_instructions\[0\].instruction,
{ source: 'cron\_job' } // Context
);
}
**效果:** 你只需要在后台配一个 JSONAgent 就会每天早上 9 点醒来查数据然后发微信“张医生P005 明天该来复查了。”
## **2\. 用户画像增强 (User Profiling)**
我们已经在 V2.8 中设计了 iit\_project\_memory (Hot Memory),现在只需要把“画像”结构化。
### **2.1 存储设计**
在 iit\_user\_preferences 表中,我们将 preferences 字段细分为两类:
1. **显性偏好 (Explicit)**:用户明确说的(“我要简报”)。
2. **隐性画像 (Implicit)**:系统分析出来的(“他是 PI关注宏观数据”
// preferences 字段内容示例
{
"communication\_style": "concise", // 简练 | 详尽
"role\_tag": "PI", // PI | CRC | CRA
"focus\_areas": \["AE", "Enrollment"\], // 关注点
"notification\_time": "08:30", // 接收时间
"feedback\_history": { // 简单的反馈记录
"thumbs\_up": 12,
"thumbs\_down": 1
}
}
### **2.2 自动画像更新 (The Profiler)**
我们在 ChatService 结束一次对话后,异步触发一个微型任务:**“画像侧写”**。
// ProfilerService.ts
async updateProfile(userId: string, lastConversation: any\[\]) {
const currentProfile \= await this.getProfile(userId);
// 让 LLM 当心理分析师
const prompt \= \`
基于刚刚的对话,更新用户画像。
对话:${JSON.stringify(lastConversation)}
旧画像:${JSON.stringify(currentProfile)}
提取规则:
1\. 如果用户抱怨"太长了",将 style 设为 concise。
2\. 如果用户问了"安全性",将 AE 加入 focus\_areas。
返回新的 JSON。
\`;
const newProfile \= await this.llm.chat(prompt);
await this.saveProfile(userId, newProfile);
}
## **3\. 反馈循环 (Feedback Loop \- Lite)**
不要做复杂的强化学习RLHF只做简单的\*\*“点赞/点踩”\*\*。
### **3.1 交互设计**
在 Agent 回复的每条消息下面(如果是小程序/网页),加两个小按钮:👍 / 👎。
### **3.2 逻辑闭环**
1. **用户点踩 (👎)**
2. **前端**:弹窗询问“哪里不好?”(选项:太啰嗦、不准确、没听懂)。
3. **后端**
* 将这条 Negative Feedback 写入 iit\_conversation\_history。
* **关键动作**:触发一次 Hot Memory 更新。
* **Prompt**"用户对刚才的回答点了踩,原因是'太啰嗦'。请在 Hot Memory 中记下一条禁令:\[针对该用户,严禁输出超过 100 字的废话\]。"
**结果**Agent 下次遇到这个用户,真的会改。这就是最真实的“智能感”。
## **4\. 多意图与任务规划增强 (Multi-Intent Handling)**
针对“先做 A再做 B”的复杂指令“查一下 P001 的状态,然后通知张医生”),我们不需要引入重型 Planner而是通过 **ReAct \+ Prompt Engineering** 实现。
### **4.1 核心策略ReAct 自然涌现**
**结论**ReAct 引擎本身就是一个轻量级 Planner。
不需要额外的 Planner 模块,只需要在 **System Prompt** 中显式教导 Agent 如何拆解任务。
### **4.2 Prompt 增强设计**
在 backend/src/modules/iit-manager/engines/ReActEngine.ts 的系统提示词中加入以下**思维链Chain of Thought指令**
const REACT\_SYSTEM\_PROMPT \= \`
你是一个临床研究助手。你可以使用工具来回答问题。
核心原则:
1\. \*\*任务拆解\*\*:如果用户请求包含多个步骤(例如"先...再...""查完...发给..."),请务必分步执行。
\- 不要试图在一个步骤里做完所有事。
\- 比如:先调用查询工具,获得结果后,再调用发送工具。
2\. \*\*依赖管理\*\*:如果步骤 B 依赖步骤 A 的结果,必须先执行 A观察 A 的结果,再执行 B。
3\. \*\*完成检查\*\*:在输出 Final Answer 之前,检查是否完成了用户的所有指令。
思考格式示例:
Thought: 用户想查 P001 并通知张医生。我有两个任务。首先,我需要查 P001 的状态。
Action: 调用 read\_clinical\_data(id='P001')...
Observation: {"status": "Enrolled"}
Thought: 我已经查到了状态。现在我需要执行第二个任务:发消息给张医生。
Action: 调用 send\_wechat\_msg(to='Dr. Zhang', content='P001 status is Enrolled')...
...
\`;
### **4.3 为什么不用 Planner**
对于 2 人团队,显式 Planner先生成计划列表再循环执行过于沉重且难以维护状态。ReAct 的 **“边走边想”** 模式完全能够覆盖 90% 的“线性多步任务”。
## **5\. 总结与建议**
| 需求 | 解决方案 | 复杂度 | 推荐实施阶段 |
| :---- | :---- | :---- | :---- |
| **主动提醒** | **Cron Skill** (复用 pg-boss \+ QC Engine) | 低 | **Phase 4** (与周报一起做) |
| **用户画像** | **Profiler Job** (对话后异步分析) | 中 | **Phase 5** (智能化增强) |
| **反馈循环** | **点赞按钮 \+ 记忆写入** | 低 | **Phase 5** |
| **多意图处理** | **ReAct Prompt 优化** (无需新模块) | 极低 | **Phase 5** (开发 ReActEngine 时同步完成) |
### **对你的建议**
1. **全盘接受 P0 建议**:主动提醒和用户画像是成本低、收益大的功能。
2. **技术上保持克制**
* 别写专门的 Alert Engine用 Cron Skill 复用现有的引擎。
* 别搞复杂的推荐算法,用 JSON 存画像就够了。
* 别搞复杂的 Planner**Prompt** 教会 ReAct 拆解任务就够了。
这样,你的 V2.6+ 架构就真正补齐了“右脑”的情商短板,变成了一个 **“有记忆、会主动、懂人情、能办事”** 的完整 Agent。

View File

@@ -0,0 +1,44 @@
## **1\. 风险预警 (Risk Alert)**
### **🔴 高风险Phase 4 排期过饱和**
* **问题**Week 4 同时安排了"视觉识别"、"周报系统"、"高级伦理规则"开发,对于 2 人团队工作量过载。
* **建议****战略性放弃视觉识别的自动录入功能**,将其降级为 MVP仅识别展示。将资源集中在"周报系统"上,因为周报对 PI 的价值感知更强。
### **🟠 中风险SOP 状态丢失**
* **问题**SopEngine 在内存中运行 while 循环。若 Node.js 服务在长任务中途重启(如发布新版),当前流程会丢失。
* **建议**:在 while 循环的每一步,增加 await updateTaskProgress(),将 currentNode 写入数据库,确保有据可查。
### **🟠 中风险AI 自动写入合规性**
* **问题**Phase 4 提到"高置信度自动录入"。在 GCP 原则下AI 直接修改源数据存在合规隐患。
* **建议**:坚持 **Shadow State (影子状态)** 原则。VisionService 的结果应生成 PROPOSED 状态的记录,必须由 CRC 人工点击确认。
## **2\. 架构优化建议 (Architecture Tuning)**
### **2.1 增加 "Dry Run" 机制**
* **痛点**JSON 配置写在数据库里,出错难排查。
* **方案**:在 ToolsService 中增加 simulate() 方法,允许在不产生副作用的情况下测试 Skill 逻辑。
### **2.2 强化字段映射管理**
* **痛点**iit\_field\_mapping 表如果只能通过 SQL 修改,运营成本极高。
* **方案**:在 Week 3 增加一个简单的 Admin API 或界面,用于管理字段别名。
## **3\. 调整后的推荐路线图 (Refined Roadmap)**
* **Week 1 (基础)**:数据库 \+ HardRuleEngine \+ ToolsService (不变)
* **Week 2 (引擎)**SoftRuleEngine \+ SopEngine (**增加状态持久化**)
* **Week 3 (联调)**:配置第一个 Skill \+ 端到端测试 (**增加 Dry Run 工具**)
* **Week 4 (扩展)**
* **P0**: 定时任务 \+ 周报系统 (Scheduler \+ Report)
* **P1**: 高级质控工具 (Visit Window)
* **P2 (延后)**: 视觉识别 (VisionService) \-\> **建议移至 V2.6**
## **4\. 结论**
架构选型Postgres-Only \+ Service-First非常精准。只需在 Phase 4 做适当减法,并注意状态持久化细节,即可确保项目按时、高质量交付。

View File

@@ -0,0 +1,169 @@
**第二章 微信服务号集成与Error 200002根因深度剖析**
用户提到的“配置消息推送失败: invalid args, 200002”且“后端无日志”是微信开发中极其典型且令人抓狂的现象。这通常不是单一的代码错误而是网络层、网关层与应用层叠加的系统性故障。
### **2.1 现象解码:“无日志”的恐怖**
当微信后台提示配置失败而您的服务器访问日志Access Log空空如也时这意味着**请求根本没有到达您的应用服务器**。
#### **2.1.1 网络层的隐形墙:防火墙与安全组**
微信服务器位于腾讯的公网IP池中。当它发起HTTP/HTTPS请求时如果您的服务器部署在阿里云、腾讯云或AWS等云平台\*\*安全组Security Group\*\*是第一道关卡。
* **诊断**大多数云服务器默认只开放22端口SSH
* **排查**检查云控制台的入站规则。必须允许0.0.0.0/0对TCP 80HTTP和443HTTPS的访问。
* **误区**不要试图将微信的IP加入白名单。微信的出口IP是不定期的海量IP池必须全量开放。
#### **2.1.2 接入层的黑洞Nginx配置错误**
如果您使用了Nginx作为反向代理请求可能在Nginx层被丢弃导致后端Java/Python应用收不到日志。
* **Server Name匹配**如果Nginx配置了server\_name www.example.com而您在微信后台填写的是直接IP地址Nginx可能因为找不到匹配的Host而直接拒绝连接。
* **SSL握手失败**如果配置的是HTTPS URL但服务器的SSL证书不完整如缺少中间证书或协议版本过旧TLS 1.0微信服务器会终止握手请求甚至不会进入Nginx日志。
### **2.2 Error 200002 “invalid args” 的代码级解剖**
如果请求成功到达服务器可以通过Nginx日志确认但微信仍然报200002则问题出在**握手协议的实现**上。这是最考验开发者基本功的环节。
#### **2.2.1 握手协议的三大铁律**
微信在验证服务器有效性时发送signature, timestamp, nonce, echostr四个参数。开发者必须完成以下步骤
1. **字典序排序Lexicographical Sorting**
* **错误高发点**很多开发者将数字类型的timestamp直接参与排序或者使用了错误的排序算法。
* **正确逻辑**:将\[token, timestamp, nonce\]放入一个字符串数组调用标准的字符串排序方法如Python的list.sort()Java的Arrays.sort())。
2. **SHA1哈希Hashing**
* **错误高发点**编码问题。在Python 3中hashlib.sha1()接受的是字节流bytes必须先对拼接后的字符串进行.encode('utf-8')。
3. **返回值的纯净性Response Purity**
* **错误高发点**这是导致200002最隐蔽的原因。微信要求**原样返回**echostr的明文。
* **忌讳**
* 不能返回JSON格式如{"ret": "success", "echo": "..."})。
* 不能包含引号(如"abc...")。
* 不能包含换行符或空格。
* **Content-Type**虽然微信不强制但最佳实践是设置Response Header为text/plain。
#### **2.2.2 深度调试代码示例Python Flask版**
以下是一个通过了生产环境验证的标准校验代码,用于替换可能存在缺陷的逻辑:
Python
import hashlib
from flask import Flask, request, make\_response
app \= Flask(\_\_name\_\_)
@app.route('/wechat', methods=)
def check\_signature():
\# 1\. 获取参数
token \= "YOUR\_TOKEN" \# 必须与微信后台填写的完全一致
signature \= request.args.get('signature')
timestamp \= request.args.get('timestamp')
nonce \= request.args.get('nonce')
echostr \= request.args.get('echostr')
\# 2\. 空值校验(防御性编程)
if not all(\[signature, timestamp, nonce, token\]):
return "Invalid Request", 400
\# 3\. 字典序排序
li \= \[token, timestamp, nonce\]
li.sort()
\# 4\. 拼接与哈希
temp\_str \= "".join(li)
hash\_str \= hashlib.sha1(temp\_str.encode('utf-8')).hexdigest()
\# 5\. 对比与返回
if hash\_str \== signature:
\# 关键使用make\_response确保返回的是纯文本不受框架序列化影响
response \= make\_response(echostr)
response.headers\['content-type'\] \= 'text/plain'
return response
else:
\# 记录错误日志以便排查
app.logger.error(f"Sig check failed. Calc: {hash\_str}\!= Req: {signature}")
return "Signature Failed", 403
if \_\_name\_\_ \== '\_\_main\_\_':
app.run(port=80)
### **2.3 “我很痛苦”的终结方案:抓包与模拟**
如果依然失败,请停止盲目修改代码,采用**证据驱动调试**。
1. **自测脚本**编写一个Python脚本模拟微信的逻辑生成签名并发送GET请求给自己的服务器。如果自测通过但微信不通过问题一定在网络层防火墙/CDN/WAF
2. **网络抓包**在服务器上使用tcpdump \-i eth0 port 80 \-w wechat\_debug.pcap。在微信后台点击提交后停止抓包并用Wireshark分析。
* 如果抓不到包 \-\> 网络不通。
* 如果抓到包但应用没日志 \-\> Nginx配置错误。
* 如果应用返回了200但微信报错 \-\> 检查Response Body是否有隐藏字符BOM头、换行符
## ---
**第三章 全场景调试指南:工具链与最佳实践**
对于“如何调试”、“本地还是远程”的疑问,行业内的最佳范式是\*\*“本地隧道调试”\*\*Local Tunnel Debugging。直接在远程服务器上修改代码并重启服务来调试微信接口效率极低且风险极高。
### **3.1 本地调试的核心逻辑:内网穿透**
微信服务器无法直接访问你笔记本电脑上的localhost:8080。因此需要一个“隧道”将公网请求转发到本地。
#### **3.1.1 工具选型与配置**
针对中国网络环境,推荐以下工具链:
| 工具名称 | 适用场景 | 优点 | 缺点 | 推荐指数 |
| :---- | :---- | :---- | :---- | :---- |
| **ngrok** | 快速验证 | 命令简单,全球通用 | 免费版域名随机变化,国内连接有时不稳定 | ⭐⭐⭐ |
| **frp** | 长期开发 | **国内标准**。需一台公网VPS。稳定域名固定 | 配置稍繁琐,需维护服务端 | ⭐⭐⭐⭐⭐ |
| **cpolar/Natapp** | 无VPS用户 | 专为国内优化,速度快 | 免费版有限制,自定义域名需付费 | ⭐⭐⭐⭐ |
#### **3.1.2 FRP实战配置SOP**
假设您有一台腾讯云/阿里云服务器IP: 1.2.3.4),这是最稳定的方案。
1. **服务端VPS配置 frps.ini**
Ini, TOML
\[common\]
bind\_port \= 7000 \# 用于frp内部通信
vhost\_http\_port \= 8080 \# 微信访问的端口
启动:./frps \-c frps.ini
2. **客户端(本地电脑)配置 frpc.ini**
Ini, TOML
\[common\]
server\_addr \= 1.2.3.4
server\_port \= 7000
\[wechat-debug\]
type \= http
local\_port \= 5000 \# 本地Python/Java服务的端口
custom\_domains \= wechat.your-startup.com \# 解析到1.2.3.4的域名
启动:./frpc \-c frpc.ini
3. **调试闭环**
* 在微信后台配置URL为http://wechat.your-startup.com:8080/callback
* 当微信发送请求时,流量路径为:微信 \-\> VPS(7000) \-\> 隧道 \-\> 本地电脑(5000)。
* **效果**您可以在本地IDEPyCharm/VSCode中打断点实时查看微信发来的XML数据包单步调试每一行代码。这是解决“痛苦”的根本途径。
### **3.2 微信官方调试工具的使用**
除了内网穿透,腾讯提供了**微信开发者工具**(主要用于小程序,但也包含公众号调试)和**在线接口调试工具**。
* **在线接口调试工具** 6
* 地址:[https://developers.weixin.qq.com/apiExplorer](https://developers.weixin.qq.com/apiExplorer)
* 用途当您的Token配置成功但消息推送失败时可以用它模拟微信服务器向您的URL发送POST请求。它会详细显示您的服务器返回了什么HTTP Code, Body帮助快速定位“回包格式错误”。
### **3.3 行业最佳实践范式**
1. **TraceID全链路追踪**在入口处生成一个UUID作为TraceID贯穿Nginx日志、应用日志和数据库日志。当某个课题组反馈机器人不回话时通过TraceID可秒级定位问题环节。
2. **日志分级与脱敏**
* **DEBUG级**打印完整的XML/JSON请求体注意生产环境需对患者姓名、ID进行掩码脱敏
* **INFO级**:记录核心链路节点(收到消息 \-\> 开始推理 \-\> 推送完成)。
* **ERROR级**记录所有的API调用非200状态码及异常堆栈。
3. **容错与重试**微信接口偶尔会出现网络抖动。在调用send接口时务必封装重试逻辑Exponential Backoff遇到超时或5xx错误时自动重试3次。
## ---

View File

@@ -0,0 +1,64 @@
# **潜在的具体风险与问题**
即使架构完美,细节仍可能翻车。以下是代码落地时可能遇到的“暗礁”以及针对性的应对策略。
## **🔴 风险一:意图路由的“延迟感”**
* **位置**IntentService (Day 20-21)
* **问题**:用户发一句话,系统先调一次 LLM (路由),再调一次 SOP/ReAct (执行)。
* **后果**:响应延迟 \= LLM(路由) \+ LLM(执行) \+ 网络开销。可能导致用户发完消息后 **5-8 秒** 才有反应。在微信的即时通讯场景下,这个体感很差,用户容易认为系统卡死。
* **对策**
1. **流式欺骗 (UX Trick)**:收到消息立刻回一个“正在思考...”或“正在查询...”的状态(企业微信 API 支持中间态更新或 typing 状态)。
2. **快速通道 (Fast Path)**:对于极短的、特征明显的指令(如“质控”、“报表”、“帮助”),先用 **正则 (Regex) / 关键词 (Keywords)** 进行拦截。只有正则拦截不住的复杂长句,再走 LLM 意图路由。
* *原则***混合路由 \> 纯 LLM 路由**。
## **🔴 风险二ReAct 的“多嘴”风险**
* **位置**ReActEngine (Day 22-23)
* **问题**ReAct 的核心机制是“思考-行动-观察”循环。模型倾向于在思考过程中输出大量中间步骤,例如:
* *AI Thought*: "用户想查 P001我需要先调用 search 工具,然后..."
* *AI Thought*: "哎呀,没查到数据,可能需要换个参数..."
* **后果**如果将这些中间思考过程Internal Monologue全部实时推送给用户用户会觉得“这个机器人话痨”、“不够干练”甚至“不自信”。
* **对策**
1. **UI 静默处理**:只把 Final Answer 发送给用户。
2. **调试可见**:中间的 Trace (思考轨迹) 只存入后台日志,或者仅在 Admin 管理端的 Debug 模式下显示,用于排查问题。
## **🔴 风险三SOP 状态机的“死锁”**
* **位置**SopEngine (Day 8-9)
* **问题**SOP 流程中经常包含“人工介入”环节。例如,如果 SOP 走到了 review\_required等待人工复核节点流程会暂停。如果 CRC 一直不点击确认,或者忘记处理,这个任务就会一直挂在内存或队列中。
* **后果**pg-boss 可能会认为任务超时Timeout触发反复重试机制最终判定失败Failed导致流程异常终止。
* **对策**
1. **区分状态**:在系统设计中严格区分 **“系统阻塞 (System Blocking)”** 和 **“业务等待 (Business Waiting)”**。
2. **挂起机制**:如果是“等人”,任务状态应设为 SUSPENDED挂起。此时应从活动队列中移除不再占用 Worker 资源。
3. **唤醒机制**:当 CRC 在前端点击“确认”后,触发一个回调,创建一个新的 Job 来唤醒流程,继续执行后续步骤。
## **3\. 给 2 人团队的“瘦身版”执行建议**
基于 V2.6 计划,为了确保在有限人力下 100% 交付核心价值,建议执行以下 **“瘦身”** 策略:
### **✂️ 裁剪清单 (不做或延后)**
1. **\[延后\] Phase 6 全阶段 (视觉能力)**:放到 V3.0 再说。目前的文本交互和结构化数据处理已经足够复杂,且 OCR 准确率调优是个无底洞。
2. **\[裁剪\] Phase 5 的语义记忆 (pgvector)**:暂时只做简单的文本记录(如 JSON 或 Markdown。跨度极大的语义召回在初期并非刚需。
3. **\[简化\] 伦理合规检测 (T11.3)**:伦理规则极度复杂且非结构化。建议先写死几条最核心的规则(如“知情同意日期必须早于入组日期”),暂不做通用的伦理引擎。
### **✨ 聚焦清单 (必须做好)**
1. **P0: ToolsService 的健壮性**:这是整个系统的地基。一定要加上 V2.3 文档中提到的 **“字段模糊映射”**,否则 LLM 根本调不通工具,体验会崩塌。
2. **P0: 每日早报 (Morning Brief)**这是用户PI感知最强的功能。一定要做得漂亮、准时这是体现 Agent “主动性”的关键。
3. **P0: 意图路由的混合模式**:正则 \+ LLM 双保险,这是保证响应速度和降低 Token 成本的关键。
## **4\. 最终评价**
这份 **V2.6 计划** 是一个经过深思熟虑的、高水平的架构设计。
* **它懂业务**SOP 状态机完美解决了医疗合规与流程锁定的痛点。
* **它懂用户**双脑架构ReAct \+ SOP解决了“机器人太死板”的交互体验痛点。
* **它懂工程**极简架构Postgres-Only \+ Service Class解决了小团队维护复杂微服务的痛点。
**结论**:只要你们能忍住不去做“视觉识别”和“复杂语义记忆”这些锦上添花的功能,按部就班地把前 5 个 Phase 做完,这将是一个在医疗垂直领域极具竞争力的产品。
**最后一步建议:**
把这份文档发给团队,然后开一个 **Kick-off Meeting**,明确告诉大家:“我们要造的是**双脑 Agent**,但我们从\*\*左脑SOP\*\*开始造,视觉能力先放进冰箱。”