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 的开发!
|
||||
436
docs/03-业务模块/IIT Manager Agent/04-开发计划/01-数据库设计.md
Normal file
436
docs/03-业务模块/IIT Manager Agent/04-开发计划/01-数据库设计.md
Normal 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 // 核心配置 JSON(SOP 流程图)
|
||||
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 2:SOP 执行与记忆表
|
||||
|
||||
### 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 5:ReAct 追踪表
|
||||
|
||||
### 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
|
||||
731
docs/03-业务模块/IIT Manager Agent/04-开发计划/02-核心引擎实现指南.md
Normal file
731
docs/03-业务模块/IIT Manager Agent/04-开发计划/02-核心引擎实现指南.md
Normal 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
|
||||
1123
docs/03-业务模块/IIT Manager Agent/04-开发计划/03-服务层实现指南.md
Normal file
1123
docs/03-业务模块/IIT Manager Agent/04-开发计划/03-服务层实现指南.md
Normal file
File diff suppressed because it is too large
Load Diff
552
docs/03-业务模块/IIT Manager Agent/04-开发计划/04-记忆系统实现指南.md
Normal file
552
docs/03-业务模块/IIT Manager Agent/04-开发计划/04-记忆系统实现指南.md
Normal 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
|
||||
281
docs/03-业务模块/IIT Manager Agent/04-开发计划/05-开发阶段与任务清单.md
Normal file
281
docs/03-业务模块/IIT Manager Agent/04-开发计划/05-开发阶段与任务清单.md
Normal 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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 Answer,Trace 存日志 | ✅ 已整合 |
|
||||
| **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、用户画像、反馈循环、多意图处理 |
|
||||
@@ -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 5:Clawbot 式记忆系统**
|
||||
|
||||
| 时间 | 任务 | 说明 |
|
||||
| :---- | :---- | :---- |
|
||||
| **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 的刁钻问题时,显得更有“记性”,同时让你们的运维工作变得无比轻松。
|
||||
@@ -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 查询和文本拼接。
|
||||
|
||||
**就用这个方案。这是目前为止最完美、最优雅的解决路径。**
|
||||
@@ -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=>
|
||||
@@ -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)**
|
||||
|
||||
### **场景 1:PI 问 "P001 入组了吗?"**
|
||||
|
||||
1. **加载 Hot Memory**:获取偏好(简洁回复)。
|
||||
2. **意图识别**:QUERY\_DATA (查数据)。
|
||||
3. **决策**:**不需要** 查历史书。
|
||||
4. **行动**:调用 read\_clinical\_data 工具去 REDCap 查实时数据。
|
||||
5. **回答**:"已入组,时间是 2026-02-05。"
|
||||
|
||||
### **场景 2:PI 问 "最近入组太慢了,我们之前有没有讨论过怎么解决?"**
|
||||
|
||||
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,当时效果有短暂提升..."
|
||||
|
||||
### **场景 3:PI 问 "我上次跟你说的那个只要看结果的规矩,你记得吗?"**
|
||||
|
||||
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。**这是系统失控时的“急停按钮”。**
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
**效果:** 你只需要在后台配一个 JSON,Agent 就会每天早上 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。
|
||||
44
docs/03-业务模块/IIT Manager Agent/05-测试文档/开发计划审核备忘录.md
Normal file
44
docs/03-业务模块/IIT Manager Agent/05-测试文档/开发计划审核备忘录.md
Normal 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 做适当减法,并注意状态持久化细节,即可确保项目按时、高质量交付。
|
||||
169
docs/03-业务模块/IIT Manager Agent/05-测试文档/微信开发技术问题咨询.md
Normal file
169
docs/03-业务模块/IIT Manager Agent/05-测试文档/微信开发技术问题咨询.md
Normal 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 80(HTTP)和443(HTTPS)的访问。
|
||||
* **误区**:不要试图将微信的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)。
|
||||
* **效果**:您可以在本地IDE(PyCharm/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次。
|
||||
|
||||
## ---
|
||||
|
||||
64
docs/03-业务模块/IIT Manager Agent/05-测试文档/潜在的具体风险与问题.md
Normal file
64
docs/03-业务模块/IIT Manager Agent/05-测试文档/潜在的具体风险与问题.md
Normal 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)\*\*开始造,视觉能力先放进冰箱。”
|
||||
Reference in New Issue
Block a user