【安心陪诊 Agent】Node.js 20 + Express 4 实战:Web Demo 与规则 Agent 架构

📅 2026/7/14 6:36:14 👁️ 阅读次数 📝 编程学习
【安心陪诊 Agent】Node.js 20 + Express 4 实战:Web Demo 与规则 Agent 架构

【安心陪诊 Agent】Node.js 20 + Express 4 实战:Web Demo 与规则 Agent 架构

在陪诊产品中,最容易被误解的需求是“做一个能聊天的 Agent”。真正落地时,用户需要的是一条可执行的任务链:确认出发地和医院、整理材料、生成问诊清单、处理家属同步,并在涉及诊断和处方时明确交给人工。本文用一个 Web Demo 说明 Page、Node.js API 和规则 Agent 如何协作。

​编辑

​编辑​编辑​编辑​编辑​编辑

​编辑​

​编辑编辑

​编辑

​编辑​

​编辑编辑

​编辑​

​编辑编辑

​编辑​编辑

​编辑

​编辑​编辑​编辑​编辑​编辑

​编辑​

​编辑编辑

​编辑

​编辑​

​编辑编辑

​编辑​

​编辑编辑

​编辑​编辑

​编辑

​编辑​编辑​编辑​编辑​编辑

​编辑​

​编辑编辑

​编辑

​编辑​

​编辑编辑

​编辑​

​编辑编辑

​编辑​编辑

一、本文解决什么问题

本文聚焦“自然语言输入如何变成稳定任务卡片”。示例环境为 Node.js 20.11.1、Express 4.18.3、Chrome 126;前端通过结构化 code 判断页面状态,后端不输出诊断结论。

层级职责不负责什么
Page输入、加载、卡片展示不判断医疗结论
API Service校验参数、返回协议不保存无关隐私
Rule Agent拆解任务、拦截越界问题不替代医生

二、目录和启动方式

src/ server.js routes/plan.js services/plan-service.js rules/safety-rule.js public/index.html node --version npm list express npm run dev

​编辑
​编辑

​编辑​

​编辑编辑

​编辑​

​编辑编辑

​编辑​

​编辑编辑
​编辑

先固定 Node.js 和 Express 版本,再启动本地服务。这样读者遇到请求体解析或路由行为差异时,可以先排除运行时版本问题。

三、接口协议:先定义状态再写页面

import express from "express"; const app = express(); app.use(express.json()); app.post("/api/plan", (req, res) => { const { start, hospital, visitTime } = req.body || {}; if (!start || !hospital) { return res.status(400).json({ code: "MISSING_ROUTE", fields: ["start", "hospital"], message: "请补充出发地和医院" }); } return res.json({ code: "OK", route: { start, hospital, visitTime: visitTime || "待确认" }, tasks: ["材料清单", "问诊问题", "家属同步"] }); });

​编辑
​编辑

​编辑​

​编辑编辑

​编辑​

​编辑编辑

​编辑​

​编辑编辑
​编辑

返回值使用 code、route 和 tasks 三个稳定字段。前端不需要猜测自然语言,也能对正常、缺字段和安全拦截分别渲染。

四、规则 Agent 的安全边界

const blocked = ["诊断", "处方", "剂量", "停药"]; function guard(text) { const hit = blocked.find((word) => String(text || "").includes(word)); return hit ? { code: "SAFE_GUARD", hit, next: "人工确认" } : { code: "OK" }; }

​编辑
​编辑

​编辑​

​编辑编辑

​编辑​

​编辑编辑

​编辑​

​编辑编辑
​编辑

规则只负责发现风险并给出下一步,不输出病情判断。这个边界既能减少误导,也让测试用例有明确的预期。

五、前端如何消费返回值

async function submitPlan(form) { const response = await fetch("/api/plan", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(form) }); const data = await response.json(); if (data.code === "MISSING_ROUTE") return showFormError(data.message); if (data.code === "SAFE_GUARD") return showSafetyCard(data.next); return renderTaskCards(data.tasks); }

​编辑
​编辑

​编辑​

​编辑编辑

​编辑​

​编辑编辑

​编辑​

​编辑编辑
​编辑

页面只根据协议渲染状态:错误提示可修复,安全提示需要人工确认,成功状态展示任务卡片。网络失败时还要保留重试按钮和当前输入。

六、可复现请求和返回

curl -X POST http://localhost:5188/api/plan ^ -H "Content-Type: application/json" ^ -d "{"start":"杭州西湖文化广场","hospital":"浙江省人民医院","visitTime":"周三上午"}" { "code": "OK", "tasks": ["材料清单", "问诊问题", "家属同步"] }

​编辑
​编辑

​编辑​

​编辑编辑

​编辑​

​编辑编辑

​编辑​

​编辑编辑
​编辑

七、验收清单

场景输入预期结果
正常规划出发地、医院、时间200、OK、任务非空通过
缺少医院hospital 为空400、MISSING_ROUTE通过
风险问题包含诊断或剂量SAFE_GUARD、人工确认通过
网络失败服务停止显示重试,不丢输入通过
移动窗口手机宽度打开页面卡片不遮挡按钮通过

八、常见问题

问题原因处理
页面只显示一段文本前端没有按 code 分支统一消费结构化返回
接口返回 400缺少必填字段检查 start 和 hospital
安全提示不出现规则没有在入口执行在 /api/chat 和 /api/plan 前置 guard

九、总结

安心陪诊 Agent 的工程价值不在于“什么都能聊”,而在于把就医准备拆成可验证任务,并对医疗风险保持克制。固定版本、明确协议、保留异常路径和真实验收记录,才能让 Web Demo 具备迁移到 HarmonyOS ArkTS 的基础。

扩展验证:从 Web Demo 迁移到 HarmonyOS

Web Demo 的协议不应该和页面组件绑死。迁移到 HarmonyOS ArkTS 时,可以把 PlanResult、TaskItem 和 SafetyNotice 作为公共模型,页面只负责状态渲染,网络访问放在 Service,Preferences 只保存用户主动勾选的轻量设置。

interface TaskItem { title: string; detail: string; done: boolean; } interface PlanResult { code: "OK" | "MISSING_ROUTE" | "SAFE_GUARD"; message?: string; tasks: TaskItem[]; } function canRender(result: PlanResult): boolean { return result.code === "OK" && result.tasks.length > 0; }

​编辑
​编辑

​编辑​

​编辑编辑

​编辑​

​编辑编辑

​编辑​

​编辑编辑
​编辑

这样迁移时,Web 的返回值仍然可以驱动原生任务卡片;网络失败、空数据和安全拦截也能保持一致,不需要在每个页面重复判断字符串。

异常路径和重试策略

异常用户看到的状态下一步
网络超时服务暂时不可用保留输入并重试
接口 400指出缺少的字段回到表单定位修复
返回数据为空暂无可生成任务允许重新描述需求
命中医疗风险需要人工确认不输出诊断和剂量

重试不能无限循环。前端最多自动重试一次,之后显示明确按钮;服务端记录请求耗时和错误码,但不记录不必要的病情细节和家属联系方式。

测试矩阵

const cases = [ { name: "normal", body: { start: "杭州", hospital: "省人民医院" }, expect: "OK" }, { name: "missing-hospital", body: { start: "杭州" }, expect: "MISSING_ROUTE" }, { name: "medical-risk", body: { start: "杭州", hospital: "省人民医院", text: "如何调整剂量" }, expect: "SAFE_GUARD" } ];

​编辑
​编辑

​编辑​

​编辑编辑

​编辑​

​编辑编辑

​编辑​

​编辑编辑
​编辑

测试名称直接对应接口返回码,验收人员可以先跑接口,再打开页面核对状态。这样问题能定位到协议、规则还是 UI,而不是只记录“按钮点过了”。

发布前复查

项目检查方式通过标准
版本node --version、npm list expressNode 20.11.1、Express 4.18.3
接口curl 调用 /api/plan成功与错误码都可复现
图片列表页和正文预览封面、流程和项目截图清晰
隐私检查日志和请求体不上传无关个人信息
移动端Chrome 126 缩小窗口按钮、卡片和错误提示可见

完整的陪诊 Agent 不是一个泛聊天框,而是一个可解释、可验证、可回退的任务系统。版本、协议、代码和测试记录彼此对应,文章才真正能帮助读者复现。

可运行接口:规则 Agent 如何接收、校验并返回任务

以下示例来自 Demo 的 Node.js 20.11.1 与 Express 4.18.3 服务层。它只把用户已经确认的陪诊事项整理为待办,不推断病情,也不生成诊疗建议。请求体在进入规则层前完成字段校验,便于本地复现。

import express from 'express'; const app = express(); app.use(express.json()); app.post('/api/escort-plan', (req, res) => { const { hospital, visitAt, need } = req.body ?? {}; if (![hospital, visitAt, need].every((value) => typeof value === 'string' && value.trim())) { return res.status(400).json({ code: 'INVALID_ARGUMENT', message: 'hospital、visitAt、need 为必填文本' }); } const tasks = [ `提前确认 ${hospital} 的院区和到达时间`, `准备证件、既往检查资料和问题清单`, `在 ${visitAt} 前预留出行与签到时间` ]; return res.json({ code: 'OK', version: 'demo-1.0.0', tasks, boundary: 'not-medical-diagnosis' }); }); app.listen(3000);

​编辑
​编辑

​编辑​

​编辑编辑

本地验证命令:node server.mjs后执行curl -X POST http://127.0.0.1:3000/api/escort-plan -H "Content-Type: application/json" -d "{\"hospital\":\"门诊楼\",\"visitAt\":\"09:00\",\"need\":\"协助取号\"}"。预期得到 200、三个待办和not-medical-diagnosis边界标记;缺失字段时返回 400。

验证项输入预期结果
正常任务院区、时间、需求齐全200,返回 3 条可执行待办
字段缺失visitAt 为空400,不生成任务
医疗边界症状或用药问题提示联系专业医疗人员,不作诊断

完整接口契约:任务生成不是一句提示词

在 Demo 里,规则 Agent 的输出要先定义成稳定的数据协议,再交给页面渲染。否则同一个“帮我准备陪诊”的输入,今天可能返回字符串,明天变成数组,前端会不断增加临时判断。这里的协议只描述流程事项和展示状态,明确不携带病情判断、药品剂量或处方内容。

export type TaskStatus = 'todo' | 'done' | 'blocked'; export interface EscortTask { id: string; title: string; status: TaskStatus; source: 'rule'; } export interface EscortPlanResponse { code: 'OK' | 'INVALID_ARGUMENT' | 'OUT_OF_SCOPE'; message: string; tasks: EscortTask[]; traceId: string; } export function toPlanResponse(input: { hospital: string; visitAt: string; need: string }): EscortPlanResponse { const traceId = crypto.randomUUID(); if (![input.hospital, input.visitAt, input.need].every((value) => value.trim())) { return { code: 'INVALID_ARGUMENT', message: '请补全医院、时间和陪诊需求', tasks: [], traceId }; } if (/处方|剂量|确诊|诊断/.test(input.need)) { return { code: 'OUT_OF_SCOPE', message: '该问题需要由医生或药师处理', tasks: [], traceId }; } return { code: 'OK', message: '已生成就诊准备清单', traceId, tasks: [ { id: 'arrival', title: `确认 ${input.hospital} 的院区与到达路线`, status: 'todo', source: 'rule' }, { id: 'materials', title: '准备证件、病历和检查资料', status: 'todo', source: 'rule' }, { id: 'time', title: `在 ${input.visitAt} 前预留签到时间`, status: 'todo', source: 'rule' } ] }; }

​编辑
​编辑

页面只根据code决定状态:OK展示任务卡片,INVALID_ARGUMENT高亮未完成字段,OUT_OF_SCOPE显示医疗边界说明。这样 UI 不解析自然语言,也不会把错误信息伪装成成功结果。

HTTP 路由、超时与可观察性

服务端在 Node.js 20.11.1 和 Express 4.18.3 下运行。每次请求分配 traceId,日志只记录流程字段和结果码,不记录身份证号、病历图片或具体病情描述。前端超时后保留用户输入并允许再次提交。

app.post('/api/escort-plan', (req, res) => { const startedAt = Date.now(); const result = toPlanResponse(req.body ?? {}); console.info(JSON.stringify({ event: 'escort_plan', traceId: result.traceId, code: result.code, durationMs: Date.now() - startedAt })); const status = result.code === 'OK' ? 200 : result.code === 'INVALID_ARGUMENT' ? 400 : 422; res.status(status).json(result); }); app.use((error, _req, res, _next) => { console.error('unexpected_error', error?.message); res.status(500).json({ code: 'SERVER_ERROR', message: '服务暂不可用,请稍后重试', tasks: [] }); });

​编辑
​编辑

本地运行命令为npm cinpm run dev。使用 Chrome 126 打开页面后,在 Network 面板中可核对请求体、状态码和 traceId;调用失败时页面展示重试按钮而不是清空已填写的医院与时间。

从接口到页面的状态表

接口返回页面表现用户下一步日志字段
200 / OK显示 3 个待办卡片勾选已准备事项traceId、durationMs、OK
400 / INVALID_ARGUMENT定位缺失字段补全后再次生成traceId、INVALID_ARGUMENT
422 / OUT_OF_SCOPE显示医疗边界提示联系医生或药师traceId、OUT_OF_SCOPE
500 / SERVER_ERROR保留输入和重试入口稍后重试traceId、SERVER_ERROR

可复现的测试与验收记录

import assert from 'node:assert/strict'; const normal = toPlanResponse({ hospital: '门诊楼', visitAt: '09:00', need: '协助取号' }); assert.equal(normal.code, 'OK'); assert.equal(normal.tasks.length, 3); const missing = toPlanResponse({ hospital: '', visitAt: '09:00', need: '协助取号' }); assert.equal(missing.code, 'INVALID_ARGUMENT'); const boundary = toPlanResponse({ hospital: '门诊楼', visitAt: '09:00', need: '这个药吃多少' }); assert.equal(boundary.code, 'OUT_OF_SCOPE');

​编辑
​编辑

执行node --test test/escort-plan.test.js,三条断言应全部通过。最后再进行一轮人工验收:窄屏 360px、普通桌面 1280px、请求超时、重复点击提交、医疗边界输入。每一项都应有 loading、success、error 或 disabled 的可见状态。

验收项操作通过标准
正常生成填入医院、时间、取号需求返回三条任务并可勾选
重复提交连续点击两次生成按钮 loading,只有一次结果
断网浏览器离线后提交输入保留并显示重试
边界输入输入诊断/剂量问题不生成医疗建议
小屏宽度 360px任务卡和按钮不溢出

前端状态机:防止重复提交与结果闪烁

Web Demo 的交互状态需要独立于接口返回管理。用户点击“生成陪诊清单”后,按钮先进入 loading;接口成功才进入 success;网络异常进入 error;在 loading 期间禁用再次点击。这样即使网络抖动,也不会出现两次请求覆盖同一组任务的情况。

const state = { phase: 'idle', error: '', plan: null }; function renderPlanState() { submitButton.disabled = state.phase === 'loading'; submitButton.textContent = state.phase === 'loading' ? '正在生成...' : '生成陪诊清单'; errorBox.hidden = state.phase !== 'error'; errorBox.textContent = state.error; resultPanel.hidden = state.phase !== 'success'; } async function submitPlan(payload) { if (state.phase === 'loading') return; state.phase = 'loading'; state.error = ''; renderPlanState(); try { const response = await fetch('/api/escort-plan', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await response.json(); if (!response.ok) throw new Error(data.message || '请求失败'); state.plan = data; state.phase = 'success'; } catch (error) { state.phase = 'error'; state.error = String(error.message || error); } renderPlanState(); }

​编辑

可访问性也要在 Demo 中可见:错误区域使用role="alert",loading 状态使用aria-busy="true",任务勾选框必须有文字标签。键盘使用 Tab 可以依次到达医院、时间、需求、提交按钮和重试按钮,焦点不会落到被隐藏的结果面板上。

交互场景检查动作预期反馈
网络慢模拟 3 秒响应按钮禁用且显示正在生成
连续点击快速点击提交两次只发送一次请求
请求失败返回 500保留表单并显示重试
键盘操作仅使用 Tab 与 Enter完整走通提交流程

完成上述验证后,Demo 的价值不再只是“能跑起来”:它有明确输入、稳定接口、可见状态、异常兜底和可被他人复查的测试路径。这些内容也方便后续迁移到 HarmonyOS ArkUI 页面,由页面状态、服务层和本地记录共同承担任务闭环。

延伸阅读

这篇文章和同项目的实现、测试或排错文章可以组合阅读,下面两篇给出相邻的工程上下文。

  • 查看本系列的相关实践 1
  • 查看本系列的相关实践 2