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:
@@ -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**
|
||||
|
||||
不要把所有逻辑都写死在质控里。我们增加一个特殊的 Skill:general\_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)。开干吧!**
|
||||
@@ -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 会通过**参数**来体现灵活性。
|
||||
|
||||
#### **工具 1:read\_clinical\_data (全能查询器)**
|
||||
|
||||
* **功能**:读取 REDCap 里的任意数据。
|
||||
* **参数**:record\_id (患者ID), fields (想查什么字段), event (哪个访视)。
|
||||
* **灵活性**:
|
||||
* 查年龄?Agent 传 fields: \["age"\]。
|
||||
* 查病史?Agent 传 fields: \["medical\_history"\]。
|
||||
* **你只需要写这一个函数,Agent 就可以查任何东西。**
|
||||
|
||||
#### **工具 2:eval\_logic (逻辑计算器)**
|
||||
|
||||
* **功能**:处理复杂的数值计算或逻辑判断(利用 json-logic)。
|
||||
* **场景**:Agent 算不清 BMI,可以调这个工具帮它算。
|
||||
|
||||
#### **工具 3:manage\_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 是否能聪明地自己填参数。
|
||||
@@ -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 落地最务实、最抗造的模式。**
|
||||
@@ -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)**:每个节点内部可以用 AI(Engine 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 的嘴(推理)。这就是既稳定又灵活的最佳实践。**
|
||||
@@ -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 \+ 意图路由)”**。
|
||||
|
||||
* 理由:视觉识别是锦上添花,而“不被当成傻子”是用户留存的关键。
|
||||
|
||||
**行动:** 请批准将“视觉识别”延后,优先开发“双脑路由”模块。
|
||||
@@ -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 的开发!
|
||||
Reference in New Issue
Block a user