免疫检查点抑制剂(ICI)在临床上很成功,但到了医院运营层面,输注中心立马变成了一个复杂系统:随机到达、随机服务时长、多站点串联、多资源并发,还有低概率但高冲击的免疫相关不良反应(irAE)。拍脑袋改流程多半是按下葫芦浮起瓢,用离散事件仿真(DES)先在虚拟世界里跑一跑,能提前看清拥堵点,对比不同策略——包括最棘手的'重症患者占着输液椅等抢救床位'这种挤兑效应。这篇文章基于 Go 实现了一个可扩展的 DES 框架,逐步加入患者分型、预约模板、药房预配、irAE 分级以及 ResusBay 抢床逻辑,最后给出核心代码。
把门诊流程抽象成可计算的图模型
通常一位 ICI 患者日间路径是:签到 → 化验 → 医生评估 → 药房 → 输注 → 观察 → 离院。这是一条串行链路,但每个节点都有各自的队列和容量(并发服务数)。真正造成排队波动的不只是服务时间长短,还有到达的聚集性、医生评估中可能发现的 irAE 以及突发事件导致的资源占用链。
我们关注的 KPI 包括:各站点平均和 P90 等待时间、全程总逗留时间、超时率(比如逗留超过 4 小时的患者比例)、以及各资源利用率。只用平均数看问题很容易被乐观带偏,尾部分位才是投诉、加班和安全风险的来源。
为什么用离散事件仿真,而不是排队论公式
DES 让系统时间只在事件发生的时刻向前跳——患者到达、开始服务、结束服务、突发 irAE、释放资源等等。这样很自然地处理串联依赖、多患者类型、优先级队列和资源耦合。相反,用 M/M/c 之类的解析解很难应对这些复杂因素,而 DES 只要在事件逻辑里描述清楚就行。
数据结构与事件机制
一个最小可用的仿真状态包括:患者对象(记录到达和各站起止时间、类型、irAE 状态)、每个站点的 FIFO 队列、每个站点的资源容量和当前占用,以及一个事件优先队列(最小堆)。最小堆让取最早事件和插入新事件都是 O(log N),对于事件驱动仿真非常高效。
资源利用率的计算:每次服务开始时直接累加服务时长到 BusyMin 里;重症 irAE 导致输液椅额外占用的那段时间,通过追加一个'释放输液椅'事件补到 BusyMin 中。这种简化在策略对比时够用,如果想画出分钟级利用率热图,可以再叠加时间片统计。
随机性:服务时长的分布选择
医疗服务时长通常有下界(不可能为负)且右偏长尾。对数正态分布天然适合这个形状,所以对签到、化验、医生、药房、观察和抢救时长都用 LogNormal 生成,并做上下界截断。输液时长则根据患者类型(短输注、长输注、联合输注)设定不同参数。实际项目中,参数需要从历史数据拟合和校准——校准精度远比分布选型重要。
策略映射到代码:预约模板与药房预配
预约模板本质上是控制到达时间序列的输入过程。仿真中实现了两种简单策略:Uniform 均匀分散在 6 小时窗口内;Staggered 让短输注集中在上午,长/联合输注安排在下午,舒缓峰值。这直接改变了队列堆积的形态。
药房预配可以简化成服务时间归零。现实中这么做的前提是提前 30 分钟配好、考虑取消率和失效窗口,这些都可以在事件层逐步扩展。
免疫治疗特有的冲击:irAE 分级与床位挤兑
轻中度的 irAE 会让观察时间延长,但严重 irAE 会触发一个连锁反应:需要抢救床位(Resus)。如果床满了,患者就带着输液椅在输注区等待转运,椅子被占着不能给下一位用,导致输注排队时间急剧上升。这就是经典的挤兑——不是床位数不足,而是床位周转卡住输液资源。
关键实现细节:发生严重 irAE 后,不是立刻释放输液椅。先留一段准备时间(15-30 分钟),然后进入 Resus 队列;等到真正拿到床位开始抢救时,才触发释放输液椅事件。这多出来的'占位时间'显著影响 P90 和超时率。
核心代码(Go)
下面贴出类型定义和核心结构体。完整的事件循环、到达生成、服务处理、策略配置等逻辑因为篇幅原因未全部展开,但基于这些类型可以很容易地搭建一个可运行的原型。
package main
import (
"container/heap"
"fmt"
"math"
)
PatientType
(
TypeShort PatientType =
TypeLong
TypeCombo
)
Station
(
SignIn Station =
Lab
Doctor
Pharmacy
Infusion
Observation
Resus
Done
)
String() {
s {
SignIn:
Lab:
Doctor:
Pharmacy:
Infusion:
Observation:
Resus:
:
}
}
IrAEGrade
(
IrAENone IrAEGrade =
IrAEMild
IrAEModerate
IrAESevere
)
String() {
g {
IrAEMild:
IrAEModerate:
IrAESevere:
:
}
}
Patient {
ID
PType PatientType
Arrive
Start [Station]
End [Station]
Wait [Station]
ServiceDur [Station]
InfusionInterrupted
InfusionReleased
InfusionPlannedEnd
InfusionHoldStartTime
}
EventType
(
EvArrival EventType =
EvServiceDone
EvIrAE
EvQueueEnter
EvReleaseInfusion
)
Event {
T
Type EventType
PID
At Station
index
}
EventPQ []*Event
Len() {
(pq)
}
Less(i, j ) {
pq[i].T < pq[j].T
}
Swap(i, j ) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index, pq[j].index = i, j
}
Push(x any) {
e := x.(*Event)
e.index = (*pq)
*pq = (*pq, e)
}
Pop() any {
old := *pq
n := (old)
e := old[n]
*pq = old[:n]
e
}
Queue []
Enq(pid ) {
*q = (*q, pid)
}
Deq() (, ) {
(*q) == {
,
}
pid := (*q)[]
*q = (*q)[:]
pid,
}
Resource {
Cap
InUse
BusyMin
}
Sim {
DayMinutes
Rand *rand.Rand
Patients []*Patient
Events EventPQ
Queues [Station]*Queue
UrgentQueues [Station]*Queue
Resources [Station]*Reso
}


