# **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 落地最务实、最抗造的模式。**