多 Agent 智能导诊项目开发记录:把“能跑”收拾成“能交付”
今天基本没再加新页面,主要是在收拾之前留下的问题。
这个项目的功能已经不少了,患者端能问诊,医生端能处理,管理端也能看数据。但真正准备提交时,还是发现了不少细节:医生已经点了完成,管理端还显示异常;接口数据一多就开始变慢;Docker 写好了却起不来;CI 虽然通过了,GitHub 还在提示 Action 版本过旧。
所以今天做的事情比较杂,但都绕着一个目标:让项目不只是我电脑上看起来能用。
先把导诊流程重新理了一遍
患者发送消息后,请求进入 /api/chat,后端会拿到当前患者和 sessionId,然后交给 TriageHarness。
现在的一次导诊大概会经过这些步骤:
患者消息和健康档案
→ 整理当前会话
→ 检索医疗知识
→ 主导诊和意图识别并行执行
→ 风险、科室、病史分析
→ 按情况追加用药或急症分析
→ Supervisor 汇总
→ 最多两轮 Review
→ 返回回答
→ 后台整理成结构化报告
Copy
最开始我也想过每次把所有 Agent 都跑一遍,但这样做没有必要,还会增加等待时间。
现在风险、科室和病史是默认节点。只有问题里确实出现药物、剂量、孕妇、儿童、胸痛、呼吸困难等内容时,才追加用药安全或急症节点。
专业 Agent 用 goroutine 并行执行,同时用一个容量为 5 的 channel 限制并发。每个专业节点最多运行 12 秒,Supervisor 最多 10 秒,整次导诊最多 45 秒。某个节点失败时只记录降级,不会直接把整个请求打断。
Review 也限制为两轮。它主要检查有没有漏掉急症提示、有没有把推测写成确诊、有没有给出不合适的用药内容。两轮之后就停止,不继续反复改答案。
RAG 这块没有硬说成向量数据库
项目里的医疗知识目前保存在 MySQL 中,每条记录带标题、正文、关键词、来源、版本、审核状态和启用状态。
查询时只读取已经审核并启用的内容:
Where("enabled = ? AND review_status = ?", true, "approved")
go
Copy
知识量现在不大,所以我没有为了简历强行接一套向量数据库。当前做法是关键词和规则打分:普通关键词命中加 4 分,急诊、120 这种比较宽泛的词只加 1 分。
这里还专门处理了否定表达。比如用户说“没有胸痛”,胸痛不能继续给急症知识加分。代码会检查关键词前面的“没有、否认、未出现、不伴、无”等内容,被否定的症状会被排除。
结果按分数排序,同一主题最多保留两条,最后取三条放进模型上下文。
每次命中的知识还会把来源、版本、匹配原因和检索时间保存到导诊记录里。这样以后知识库内容改了,打开旧报告时仍然能知道当时参考了什么。
如果没有检索到知识,风险评估和急症判断照常运行。我不希望因为知识库少一条内容,系统就跳过必要的安全提醒。
最麻烦的不是模型,是三个端的状态
今天修得比较久的是医生端、患者端和管理端之间的状态同步。
之前出现过一种情况:医生已经点击“确认处理完成”,医生端看着没问题,但管理端仍然显示异常升级,患者端也看不到医生的处理结果。
原因很直接:不同页面只改了自己那张表,没有把同一次导诊相关的数据一起处理。
现在导诊记录、Agent 轨迹、人工复核工单、预约和随访都用 sessionId 关联。医生完成处理后,后端会一起更新导诊状态、工单、异常随访和患者通知,同时保存处理人、处理时间和医生意见。
人工复核工单也限制了状态顺序:
pending → accepted → replied → closed
Copy
没接单不能回复,状态不对时后端直接返回 409。这比只把前端按钮禁用更稳,因为接口本身也不会接受错误操作。
目前有些跨表更新还是顺序执行,并没有全部放进事务。个人项目阶段先用状态校正和验收脚本兜住,后面如果继续往正式部署方向做,这部分要补事务,通知类操作可以再考虑 Outbox。
接口慢了以后,开始补性能账
功能刚做出来的时候数据少,很多问题看不出来。后面报告、随访和 Agent 轨迹越来越多,管理端开始明显变慢。
排查下来,主要是几个老问题:同一个患者档案在列表里被反复查询;仪表盘每个数字都单独查一次;Agent 轨迹列表直接返回完整长文本;部分只读接口还会反复刷新随访状态。
这次把患者档案查询做了复用,仪表盘改成条件聚合,列表只查页面真正需要的字段。Agent 轨迹则先返回摘要,点开详情时再加载完整内容。MySQL 连接池也补上了最大连接数、空闲连接数和连接回收时间。
我写了一个简单的 Go 压测工具,测试 10 个只读接口,每个接口请求 50 次,并发数为 10,总共 500 次。
| 接口 | 优化前 P95 | 优化后 P95 |
|---|---|---|
| 管理端仪表盘 | 4211.5 ms | 684.9 ms |
| 管理端分诊记录 | 4362.3 ms | 538.6 ms |
| 管理端 Agent 轨迹 | 7134.7 ms | 2151.1 ms |
| 患者分诊记录 | 2944.5 ms | 1218.0 ms |
500 次请求的错误率是 0。
不过这只是本地 Windows 和本地 MySQL 的结果,没有计算模型、地图和公网耗时,所以只能说明数据库和接口查询比之前顺了,不能拿来证明系统已经能扛医院级并发。
把启动配置收紧了一点
项目原来为了开发方便,部分账号和数据库配置有默认值。自己电脑上没什么问题,但如果换个环境,很容易忘记修改。
现在所有环境启动时都要求配置:
BaseUrl
APIKey
MYSQL_DSN
Copy
当 APP_ENV=production 时,还会继续检查医生账号、管理员账号、密码、百度地图 AK 和短信配置。密码不能继续使用项目里的演示值,配置缺失时服务直接停止启动。
真实 .env、模型密钥、数据库连接、短信密钥和地图 AK 都已经放进 Git 忽略规则。提交之前我又扫了一遍,仓库里没有出现之前使用过的真实 AK。
Docker 真跑起来以后才发现端口撞了
Dockerfile 用了两阶段构建。第一阶段编译 Go 程序,第二阶段只留下二进制和运行需要的证书、时区、curl、中文字体。应用不是用 root 跑,而是单独建了 UID 10001 的用户。
Compose 里放了 Go 服务和 MySQL 8.4。MySQL 先通过健康检查,应用再启动。
应用本身也加了两个接口:
/health 检查 HTTP 进程
/ready Ping MySQL,失败时返回 503
Copy
一开始镜像能构建,我以为这部分差不多结束了。真正执行 docker compose up 时才发现本地的 aggo-sse.exe 已经占用了 8080,Docker 无法再绑定同一个端口。
报错内容很直接:
ports are not available: listen tcp 0.0.0.0:8080
Copy
为了不关掉原来的本地数据环境,我把 Docker 默认宿主机端口改成了 8081:
ports:
- "${DOCKER_PORT:-8081}:8080"
yaml
Copy
现在本地服务继续用 8080,Docker 环境使用 8081,两边可以同时打开。
CI 没做复杂,只做我能讲清楚的部分
GitHub Actions 目前只做四件事:
go test
go vet
go build
docker build
Copy
没有自动发布,也没有去连接真实数据库。原因也很简单:项目现在没有正式服务器,硬加自动部署除了让简历看起来热闹,面试时还会给自己增加解释成本。
第一次 CI 实际上已经跑成功了,但 GitHub 提示部分 Action 还基于 Node 20。随后把 Checkout、Setup Go 和 Docker Build Action 更新到了对应的新版本,又推了一次。第二次 Go 检查和 Docker 构建都正常通过。
提交前最后检查了一遍
这次最终检查的结果是:
- 患者端、医生端、管理端和健康接口验收 8/8 通过;
- Go 测试覆盖率 31.8%;
go vet通过;- Docker 镜像实际构建成功;
- Compose 配置可以正常解析;
- 本地 8080 和 Docker 8081 都能访问;
.env、日志、二进制和真实密钥没有进入提交;- 代码已推送到 GitHub,CI 运行成功。
今天没有做出什么新的大页面,但项目比昨天更像一个完整项目了。
以前比较关注“功能有没有”,今天更多是在处理“状态能不能对上、出错后怎么办、换个环境能不能启动、提交后有没有人替我检查”。这些东西在页面上不一定显眼,但少一个都会在后面出问题。
接下来暂时不准备继续加业务。先把 Harness、登录鉴权和跨端状态更新相关测试补一些,再把项目里的技术细节整理成自己能讲明白的话。
更多推荐

所有评论(0)