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

【安心陪诊 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 迁移到 HarmonyOSWeb Demo 的协议不应该和页面组件绑死。迁移到 HarmonyOS ArkTS 时可以把 PlanResult、TaskItem 和 SafetyNotice 作为公共模型页面只负责状态渲染网络访问放在 ServicePreferences 只保存用户主动勾选的轻量设置。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 ci、npm run dev。使用 Chrome 126 打开页面后在 Network 面板中可核对请求体、状态码和 traceId调用失败时页面展示重试按钮而不是清空已填写的医院与时间。从接口到页面的状态表接口返回页面表现用户下一步日志字段200 / OK显示 3 个待办卡片勾选已准备事项traceId、durationMs、OK400 / INVALID_ARGUMENT定位缺失字段补全后再次生成traceId、INVALID_ARGUMENT422 / OUT_OF_SCOPE显示医疗边界提示联系医生或药师traceId、OUT_OF_SCOPE500 / 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 中可见错误区域使用rolealertloading 状态使用aria-busytrue任务勾选框必须有文字标签。键盘使用 Tab 可以依次到达医院、时间、需求、提交按钮和重试按钮焦点不会落到被隐藏的结果面板上。交互场景检查动作预期反馈网络慢模拟 3 秒响应按钮禁用且显示正在生成连续点击快速点击提交两次只发送一次请求请求失败返回 500保留表单并显示重试键盘操作仅使用 Tab 与 Enter完整走通提交流程完成上述验证后Demo 的价值不再只是“能跑起来”它有明确输入、稳定接口、可见状态、异常兜底和可被他人复查的测试路径。这些内容也方便后续迁移到 HarmonyOS ArkUI 页面由页面状态、服务层和本地记录共同承担任务闭环。延伸阅读这篇文章和同项目的实现、测试或排错文章可以组合阅读下面两篇给出相邻的工程上下文。查看本系列的相关实践 1查看本系列的相关实践 2