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 的开发!