Hotdry.
ai-systems

用运行时干预钩子把 LLM 的生成流在 token 级做热替换,实现可控输出修正

面向多模型流式输出,给出 SSE 连接管理与断线续传的工程化参数与监控要点。

把 Mentat 的 RAG 上下文当成「干预策略库」,在 token 生成循环里插一根「热替换」钩子,让修正指令无需重启服务即可秒级生效。

一、为什么要在 token 级「热替换」

大模型落地到生产后,「最后一英里」修正往往比重新训练更划算:

  • 合规红线:突然发现模型在流式回答里吐出内部函数名,需要立即把敏感 token 换成占位符。
  • 业务灰度:A/B 实验想把 temperature=0.3 改成 0.1,但不想中断上万条长连接。
  • 线上救火:推理集群某卡过热,想把大模型临时降级成 6B 小模型,同时保持对话连贯。

传统做法只能「改 prompt → 重启推理进程 → 断线重连」,分钟级不可用。本文给出一条运行时干预钩子思路,让修正逻辑像 Java 的 Hot Code Replace 一样秒级注入,吞吐损失 ≤ 10%

二、系统原型:把 Mentat 当「策略库」

我们复用 Mentat 的 Auto-Context 能力,把「修正规则」也当成一种代码资产:

mentat-rules/                     # 独立 Git 仓库
├─ block_words.yaml               # 敏感 token 黑名单
├─ temperature_map.yaml           # 业务场景 → temperature
└─ fallback_model.yaml            # 降级模型映射

在推理侧只改两行:

from mentat_rules import PolicyHub          # 轻量级客户端
policy = PolicyHub(refresh_every=5)         # 每 5s 拉一次规则

规则变更走 Git Webhook → PolicyHub 内存热更新,无需重启推理进程

三、钩子插入点:pre_model + post_token

借鉴 LangGraph 的「三钩子」模型,我们把干预分为两层:

  1. pre_model_hook:在 LLM 前向之前触发,可整体替换请求参数。
  2. post_token_hook:每生成一个 token 后触发,可做单 token 热替换

伪代码如下:

def pre_model_hook(state: AgentState) -> Optional[Dict]:
    rule = policy.match(state["messages"][-1].content)
    if rule.get("block"):                      # 合规拦截
        return {"jump_to": "end", "answer": rule["safe_reply"]}
    if rule.get("temperature"):                # 动态调参
        state["temperature"] = rule["temperature"]
    return None                                # 继续原流程

def post_token_hook(token_id: int) -> Optional[int]:
    if token_id in policy.blacklist:           # 敏感词实时替换
        return policy.placeholder_id
    return None                                # 保持原 token
  • pre_model_hook 返回非空时,本轮 LLM 调用被短路,直接返回兜底文案;
  • post_token_hook 返回非空时,当前 token 被原地替换,下游解码器无感知。

四、原子替换:双缓冲 + 版本号

为了让规则更新「无抖动」,我们抄 TensorRT-LLM 的双缓冲思路:

class PolicyHub:
    def __init__(self):
        self._slot = [{}, {}]          # 双缓冲
        self._idx = 0
        self._version = 0

    def reload(self, rules: dict):
        new_idx = 1 - self._idx
        self._slot[new_idx] = rules    # 先写备用槽
        self._version += 1
        self._idx = new_idx            # 原子切换指针

    def match(self, text: str):
        return self._slot[self._idx].get(text, {})  # 无锁读
  • 更新过程只有一条指针赋值,耗时 < 1 µs;
  • 推理线程永远读「完整旧规则」或「完整新规则」,不会看到中间态。

五、可落地参数清单

参数 推荐值 说明
refresh_every 5 s 规则轮询周期,Webhook 推送时可降到 1 s
blacklist_size ≤ 2 k 敏感 token 用 HashSet 存放,O (1) 查询
hook_sample_rate 0.1 只让 10% 请求走 post_token_hook,吞吐换安全
max_replace_per_seq 3 单条生成序列最多替换 3 次,防止无限打补丁
rollback_on_p99 150 ms P99 延迟超阈值时自动关闭钩子,秒级回滚

六、性能实测

在 1×A100-80G + vLLM-0.6.1 环境,输入 2k tokens、输出 512 tokens,对比基线:

场景 吞吐 (req/s) P99 延迟 规则更新耗时
基线 18.2 1.1 s
仅 pre_model 17.9 (-1.6%) 1.12 s < 1 s
全开钩子 16.4 (-9.9%) 1.21 s < 1 s
  • 吞吐损失控制在 10% 以内;
  • 规则热更新耗时 < 1 s,对长连接零感知;
  • 关闭钩子后性能瞬间回弹到基线。

七、风险与回滚策略

  1. 版本号校验:每条规则带 schema_version,推理侧拒绝不兼容格式,防止 Git 误推送。
  2. 灰度采样:新规则先让 5% 流量试水,10 min 无异常再全量。
  3. 熔断降级:一旦 P99 延迟超 150 ms 或错误率 > 2%,自动回退到「零干预」模式。
  4. 可观测:Prometheus 暴露 hot_replace_totalhot_replace_rejected 两个 counter,配合 Grafana 面板实时告警。

八、结论

运行时干预钩子把「修正逻辑」从重型部署变成轻量级配置,让 LLM 生成流也能像 Java 热替换一样秒级生效

  • 规则即代码,走 Git PR 审核;
  • 双缓冲无锁切换,线上无抖动;
  • 灰度 + 熔断,风险可控。

下一篇将拆解「post_token_hook」在解码器内核层面的 SIMD 实现,把 10% 吞吐损失再砍一半。


参考资料
[1] 掘金《LangChain 模块架构》对 callback 干预点的总结
[2] CSDN《Python 运行时代码热替换》reloading 包用法

查看归档