面试知识库

01 · Agent 主循环与 Harness#

简历原话:Agent 主循环与 Harness:跨平台检索通过同轮多工具调用并发执行;基于 AgentScope 中间件封装 6 个 Hook 切点,注册 5 类规则约束执行轨迹,写操作经三级拦截;修复用户中途切换品类时反复检索、反复被拦截的死循环。

30 秒口述版#

ShoppingX 是单模型单环的购物 Agent:一个主模型持有全部业务工具,按「想 → 调工具 → 看结果 → 判断够不够」循环,直到自己调收尾工具。跨平台检索不派子 Agent,而是模型在同一轮里发多条检索调用,每个平台或每个套装槽位一条,框架把标了并发安全的调用合成一批并发执行。控制面我基于 AgentScope 的模型中间件和工具中间件封装了 6 个 Hook 切点,上面注册终止、安全、预算、重复、漂移 5 类规则;写操作要过只读标记、权限引擎、用户确认卡三级拦截,模型本身没有真正下单的能力。印象最深的是一个死循环:用户在长会话里从背包换到颈枕,planner 输出校验失败,收尾闸又只认成功过的 planner,模型反复重搜、反复撞闸,一轮 58 次模型调用、180 秒超时;修完后同一轮重放 4 次调用、25 秒正常收尾。

背景与问题#

1. 为什么是单环,不是多 Agent。 项目早期跟着教学稿做过「主 Agent 派子 Agent」:先是同质克隆,后来改成检索 worker + 交易 worker 的读写切分。上线后拉日志统计:交易 worker 440 个会话里 0 次被派发;检索 worker 60 次派发全是「派出去只调一次检索就回来」的空壳,没有一次真用到隔离上下文。子 Agent 带来的只有多一层模型调用和多一份上下文拼装成本。所以收回成一个主环,并行需求改由「同一轮多发几条工具调用」承担。

2. 单环之后,模型的自由度变大,轨迹容易跑偏。 线上看到过的典型现象:

  • 不收尾:模型拿到候选以后还想「再找找更好的」,一轮拖到二三十次调用。
  • 收尾后继续动:调完出清单的工具又去调检索,或同一轮连下两单。
  • 重复打转:同一个检索词换个说法反复搜,每次召回同一批商品。
  • 慢慢偏题:每一步看着都合理,五六步以后已经在搜一个用户没提过的品类。
  • 越权写:用户说「把上次那单取消了」,模型直接编一个订单号去调取消。

这些问题靠 prompt 写「请不要……」压不住:prompt 改一句,别的场景就回退;而且规则散在 prompt 里,没法单独测试、单独统计触发次数。

3. 规则也会出 bug,出了就是死循环。 2026-09-25 跑一个 20 轮的长会话压测,第 7 轮用户说「配套买一个旅行用的颈枕」(前几轮在聊双肩包)。现象:

  • planner(把自然语言拆成结构化意图的工具)连续输出不合法字段:把品类域写成枚举外的值,把预算金额写成字符串 “None”,整份结构化输出校验失败。
  • 模型想收尾,收尾闸说「本轮还没调过 planner,候选是上一轮的」,拒绝。
  • 模型回头重调 planner,还是失败;换个检索词重搜一遍,再去收尾,再被拒。
  • 循环检测没拦住:它按「同名工具在滑动窗口里出现几次」计数,模型在 planner、检索、精挑、收尾四个工具之间轮换,哪个都没到阈值。
  • 最后是整轮超时终止:58 次模型调用、180 秒、累计 76.6 万 prompt token,用户看到的是「处理了很久仍未完成」。

做法与取舍#

1. 并行:同轮多工具调用,不派子 Agent#

  • 检索工具的入参可以指定平台或套装槽位:跨平台比价时模型一轮发 3~5 条,每条一个平台;「旅行三件套」这类套装需求一轮发 3 条,每条一个槽位。
  • 工具注册时声明「并发安全」和「只读」。框架拿到一条 AI 消息里的多个调用后,把相邻的并发安全调用合成一个批,用协程并发跑;声明了非并发安全的工具会单独成批、串行执行。
  • 本项目的工具全部声明为并发安全,包括下单工具:它只生成确认卡,两张卡之间互不影响,没必要串行。代价是所有「按轮记状态」的规则都要能承受同一批里的并发交错,Q3 那个 bug 就出在这里。
  • 同一批调用共享一个「批次号」(这一轮模型调用的序号)。所有按轮计数的闸都以批次为单位裁决,避免同一次决策被并发执行顺序切开(见第 4 点的反例)。

为什么这样选:并行的本质需求是「几件互不依赖的检索同时跑」,不需要各自的推理过程。同轮 batch 省掉子 Agent 的一次模型往返,上下文也只在主环维护一份,前缀缓存不被打散。验收时真 LLM 跑 4 遍,每遍同轮发 3~5 条、0 次派发。

否掉的方案:

  • 保留检索子 Agent:上面的数据已经说明它是空壳;而且子 Agent 需要自己的深度限制、预算分账、结果回传格式,多一整套失控防线。
  • 写死「并发搜所有平台」的确定性流程:用户经常只想搜一两个平台,或者只买一件,固定全平台扇出浪费检索预算;让模型决定发几条,框架只负责并发执行。

2. 6 个 Hook 切点:把 Agent 生命周期接到 AgentScope 中间件上#

AgentScope 2.0 提供两种中间件:挂在 Agent 上的(能包住一次模型调用、一次推理、一次回复),挂在工具上的(洋葱模型,包住一次工具执行)。我在上面封装了一层统一的 Hook 注册表,对外只暴露 6 个切点:

切点挂在哪典型用途
装配 system prompt组装 Agent 时跑一次把策略块追加到 system 段末尾
模型调用前模型中间件,调用前看门狗、按成本换模型档位、裁掉最旧的工具返回
工具调用前工具中间件,进入下一层之前所有硬闸:终结后禁止新动作、预算、顺序前置、熔断
工具调用后工具中间件,拿到结果之后截断、外部内容围栏、注入过滤、在结果尾部追加提示
推理后推理结束、AI 消息已写进状态漂移检测、「没调收尾工具就想结束」的纠正
回合结束一次回复结束前最终回答去掉内部提示文案、脱敏

规则注册时声明切点、名字、优先级。同一个切点按优先级从小到大执行;工具调用前的规则抛「拒绝」信号就立即停下,被拒的调用不执行,拒绝理由作为工具结果回给模型。

为什么选这 6 个点:落点是用探针逐个试出来的。例如「模型调用前」只能挂在模型调用那一层,因为只有那里拿得到完整 messages;「推理后」要挂在推理结束处,因为此刻本轮 AI 消息已经写进状态,能如实判断「这轮到底调没调工具」。system prompt 单独一个装配期切点,是因为策略要对每一轮都生效,拼进 system 段末尾比每轮插一条消息省 token,也不破坏前缀缓存。

否掉的方案:

  • 直接用框架原生的中间件写业务规则:每条规则都要自己处理洋葱调用、状态读写和异常,规则一多,顺序关系散在各处。统一注册表之后,顺序契约写在一张表里(例如截断必须早于追加提示,否则刚贴上的提示会被截掉)。
  • 按切点分文件:一个关切天然跨三四个切点(终止要在调用前拦、调用后置位、推理后催),按切点分会把一件事拆到四个文件。所以按关切分文件,每个文件内写清楚它在哪些切点上挂了什么。

失败策略:单条规则自身抛异常时,记日志并跳过(fail-open),不让控制面的 bug 拖垮主链路。安全类规则都是纯集合查找和正则,异常面接近零;以后如果加依赖外部服务的安全规则,要在规则内部自己改成失败即拒绝。

3. 5 类规则:约束执行轨迹#

类别管什么硬拒 / 软提示
终止该停就停:收尾后禁止新动作;纯文字结束没调收尾工具就当场重发一次;45 秒无进展先发收尾指令、再不动就硬停并交出部分结果;收尾资格(没候选、本轮没规划、没精挑不许出清单)硬 + 软
安全外部网页内容过滤提示词注入并包进围栏;超长结果截断;最终回答脱敏;取消订单前必须先查单硬,无逃生门
预算每次任务检索总量 8 次、网页搜索 2 次、深度研究 6 条搜索,三本账互不透支;按累计成本分档,高档收走放大成本的工具事实类硬拒,推定类可逃生
重复滑动窗口 6 次里同名工具无进展出现 4 次,在结果尾部提示换思路;同一工具连续失败 3 次熔断 60 秒软 + 熔断
漂移每 3 轮检查四类信号:目标遗忘、连续空结果、命中用户黑名单、token 消耗突增;轻微提醒、严重强纠正、连续严重强制收尾分级

另外还有几条不算「约束」的钩子:顺序前置的软提醒、上下文塑形(偏好注入、旧工具结果换占位)。

两个关键设计:

  • 判据读事实,不 grep 文本:本轮调过哪些工具、候选登记表里有几件、精挑定稿几件,都由适配层从可靠数据源填进上下文。早期有过一个四阶段状态机(规划 → 检索 → 比价 → 收尾),它只是这些事实的影子,却要自己维护转移顺序和回退,最后变成要和事实对账的第二套真相,后来删了。
  • 效率闸有逃生门,安全闸没有:依据推定的闸(例如 planner 判断「这轮不需要上网搜」就拦网页搜索)可能立错墙。同一堵墙连拒 2 次后放行,并且放开后不再关(闩锁)。依据事实的闸(预算用完、已经收尾)永远硬拒。逃生计数同样按批次算:同一条 AI 消息里并发的 4 个调用是一次决策,只记一次拒绝。

否掉的方案:所有闸一律硬拒。硬拒在推定出错时就是死锁,模型只能换措辞反复撞;所有闸一律软提示。软提示对「已经下定决心的模型」没用,取消订单、预算这类代价不对称的事必须硬。

4. 写操作三级拦截#

写操作指下单、取消订单这类改变外部状态的工具。三级各管一件事:

  1. 只读标记(工具注册层):每个工具注册时声明是否只读。AgentScope 在默认权限模式下,对非只读工具一律挂起、发「需要用户确认」事件。新加一个写工具时,如果作者忘了处理,它默认就是被挂起的,失效方向是「多问一次」,不是「悄悄执行」。
  2. 权限引擎精准放行 + 写路径硬闸(执行层):Agent 自己工作流里的写零件(提问、存记忆、几个收尾工具、两个交易工具)按工具名逐个写进放行表,不用整档关掉权限的 BYPASS 模式;新写工具不进这张表就走不通。Harness 在工具调用前再加两道硬闸:取消前本轮必须查过单(防止模型编订单号撞上别人的真单);本批已收尾后,下一批任何工具一律拦下(防连环下单)。
  3. 确认卡 + HTTP 决议(业务层):交易工具本身只生成一张持久化的确认卡,真正写订单的动作只能由用户在页面上点按钮、走 HTTP 接口完成,模型手里没有落订单的通路。确认卡里的商品 id 只接受本会话候选登记表里出现过的 id;同一轮同一组商品算出同一个幂等指纹,模型重投只会得到同一张卡;卡 30 分钟过期。

为什么是三级而不是一级:每一级防的失败不一样。第一级防「新工具忘了处理」;第二级防「模型顺序错、在该停的时候继续动」;第三级把最终决定权放在人手里,即使前两级都被绕过,也写不出订单。

否掉的方案:

  • prompt 里写「下单前先问用户」:模型不一定照做,且无法审计。
  • 直接用框架原生的挂起确认:它会把整个回复挂在半路等人,用户体验是 Agent 卡住;确认卡是异步的,Agent 这一轮正常结束,用户什么时候点都行。
  • BYPASS 模式:省事,但以后加的真写工具会被一起悄悄放行。

5. 修复「中途切换品类」的死循环#

根因拆成三层:

  • 上游输出不稳:planner 的输出里有一个 21 值的品类域枚举字段,只为「换品类时清空上一轮的偏好词表」这一个用途存在。模型碰到枚举外的品类(旅行颈枕)就自造一个值,整份结构化输出校验失败;另外模型会把空值写成字符串 “None”,数值字段解析失败。
  • 闸只认成功,不认尝试:收尾资格闸问的是「本轮 planner 成功过没有」。planner 一直失败,这个条件永远不满足,而这道闸没有逃生门——它属于「收尾资格」,是硬闸。
  • 重复检测看不见跨工具的循环:循环检测按同名工具计数,看门狗按「有没有进展」计时;模型每次重搜都带回新候选,算作进展,看门狗被一次次重置。

修法三刀:

  1. 闸改读「尝试过」的事实:会话状态里单独记一份「本轮以错误结束的工具」,收尾闸看到 planner 本轮失败过,就不再要求它(精挑那一条照查——没精挑就没有清单来源)。原则是:闸不能把模型往一个一直失败的工具上赶。
  2. 上游字段归一:字符串 “None” / “null” 在解析前统一当缺席处理;更根本的是把品类域枚举整个删掉,换成一个布尔字段「本轮是否换了品类」,由 planner 对照上一轮品类直接判断。这样不存在「枚举外的值」这种失败形态。
  3. planner 判品类先看本轮原话:线上另一例是用户第 3 轮说「推荐一个保温杯」,planner 却沿用了上一轮的「商务通勤背包」,下游就按背包去搜、去精挑,再被漂移和收尾闸拦。原因是 prompt 里字段说明带了一个示例品类值,模型会照抄,而且上一轮的品类信息放在显眼位置。改为在拼上下文时明确「品类先看本轮原话,本轮点名了就换」,并去掉会被照抄的示例值。

否掉的方案:

  • 给收尾闸也加逃生门:能解开这次死锁,但会让「没规划就收尾」的错误在别的场景漏过去;问题在判据错了,不在闸太硬。
  • 调大循环检测阈值或改成跨工具计数:跨工具计数会误伤正常的「搜 → 精挑 → 再搜」曲折;那是在让问题测不出来,而不是不发生。
  • planner 失败就整轮报错:用户这一轮就白问了;planner 的结构化字段是辅助信号,缺了它模型仍能靠原话检索。

流程图 / 架构图#

图 1:主循环与 6 个 Hook 切点#

flowchart TD
    A["装配 system prompt<br/>切点1:追加策略块"] --> B["模型调用前<br/>切点2:看门狗 / 成本分档 / 裁旧结果"]
    B --> C["主模型推理<br/>一条 AI 消息可含多个工具调用"]
    C --> D["推理后<br/>切点5:漂移检测 / 未收尾纠正"]
    D -->|有工具调用| E["框架分批<br/>相邻并发安全调用合成一批"]
    D -->|没调收尾工具就想结束| B
    E --> F["工具调用前<br/>切点3:终结闸 / 顺序硬闸 / 收尾资格 / 预算 / 熔断"]
    F -->|拒绝| G["拒绝理由作为工具结果回给模型"]
    F -->|放行| H["并发执行本批工具"]
    H --> I["工具调用后<br/>切点4:截断 / 围栏 / 注入过滤 / 追加提示 / 置位"]
    G --> B
    I -->|调的是收尾工具| J["回合结束<br/>切点6:去内部文案 / 脱敏"]
    I -->|非收尾工具| B
    J --> K["返回清单给用户"]

图 2:写操作三级拦截时序#

sequenceDiagram
    participant M as 主模型
    participant F as 框架权限层
    participant H as Harness 工具调用前
    participant T as 下单工具
    participant DB as 确认卡表
    participant U as 用户页面
    participant API as HTTP 决议接口
    M->>F: 调用下单工具 带商品 id
    F->>F: 一级 非只读工具默认挂起<br/>查放行表 命中才继续
    F->>H: 二级 执行层硬闸
    H->>H: 本批之前已收尾? 取消前查过单?
    H-->>M: 不满足则拒绝 理由回给模型
    H->>T: 满足则执行
    T->>T: 商品 id 必须在候选登记表内<br/>计算幂等指纹
    T->>DB: 写一张待确认卡 30 分钟有效
    T-->>M: 返回确认卡 本轮结束
    U->>API: 三级 用户点确认
    API->>DB: 校验归属与有效期 生成订单

图 3:死循环修复前后#

flowchart LR
    subgraph 修复前
    A1["用户:配套买个旅行颈枕"] --> B1["planner 输出越界品类域<br/>校验失败"]
    B1 --> C1["模型换词重搜<br/>带回新候选 看门狗重置"]
    C1 --> D1["调收尾工具"]
    D1 --> E1{"收尾闸:<br/>planner 成功过?"}
    E1 -->|否 硬拒| B1
    end
    subgraph 修复后
    A2["用户:配套买个旅行颈枕"] --> B2["planner 只判 换品类=是<br/>没有枚举可越界"]
    B2 -->|万一仍失败| F2["记入 本轮失败工具"]
    B2 --> C2["同轮检索 + 自动比价精挑"]
    F2 --> C2
    C2 --> E2{"收尾闸:<br/>planner 成功或尝试过<br/>且精挑过?"}
    E2 -->|是| G2["出清单 4 次调用 25 秒"]
    end

图 4:效率闸的逃生门(按批次计数、闩锁放行)#

stateDiagram-v2
    [*] --> 拦截中: 推定类闸命中
    拦截中 --> 拦截中: 新一批仍调同一目标 拒绝计数加一 同批只记一次
    拦截中 --> 已放行: 连拒达到 2 次
    已放行 --> 已放行: 后续同一目标直接放行 计数不清零
    note right of 已放行
        只放开效率类禁令
        预算闸 终结闸 仍然硬拒
    end note

数字怎么来的#

这条 bullet 的数字都是「结构计数」,另外配了几组复现和验收的实测数,面试时用来支撑。

数字口径
6 个 Hook 切点注册表里允许注册的切点就这 6 个,注册时写错切点名直接报错。曾经有过第 7 个「会话开始」,它上面唯一的规则挪回编排层后,空切点一起删了。
5 类规则按关切分:终止、安全、预算、重复、漂移。顺序前置提醒和上下文塑形也挂在切点上,但它们不拦截、不改轨迹走向,不算进约束规则。
三级拦截只读标记(注册层)、权限引擎放行表 + 写路径硬闸(执行层)、确认卡 + HTTP 决议(业务层)。三级各自有单测:非只读工具必须全部出现在放行表里;取消前未查单必被拒;确认卡商品 id 不在候选登记表里必被拒。
单环依据 440 / 60 / 02026-09-16 拉线上会话日志统计:交易 worker 440 个会话 0 次派发;检索 worker 60 次派发,逐条看轨迹,全是「只调一次检索就返回」;需要隔离上下文的定点调查 0 次。
同轮并发验收 4/4改成单环后用真实模型跑跨平台 query 4 遍,每遍都在同一轮发出 3~5 条检索调用,0 次派发。
死循环 58 次 / 180 秒 / 76.6 万2026-09-25 的 20 轮长会话压测(双肩包 → 颈枕 → 绑带 → 套装 → 耳机 → 总结),第 7 轮的调用日志:模型调用次数、整轮墙钟时间(撞上整轮超时)、这一轮累计 prompt token。
修复后 4 次 / 25.4 秒用同一会话前 6 轮的状态恢复,重放第 7 轮原话,真实模型跑,正常出清单。之后整段 20 轮重跑一遍,没有再出现超时轮。
换品类判对率 1/5 → 9/10线上会话「推荐一个保温杯」这一轮,用前两轮状态本地重放:修前 5 次错 4 次(沿用背包),修后 10 次对 9 次;同时回归「商务一点的」「再便宜点」这类不该换品类的追问,5/5 正确沿用。

几个常量,追问时可能被问到:

  • 主环最大迭代 30 次,整轮超时 300 秒(压测那一轮的 180 秒是当时压测脚本的配置)。
  • 循环检测:最近 6 次调用里同名工具无进展出现 4 次。
  • 工具熔断:连续失败 3 次,打开 60 秒。
  • 看门狗:45 秒无进展先发收尾指令,再无进展硬停交部分结果。
  • 检索预算:每次任务检索 8 次、网页搜索 2 次、深度研究 6 条搜索。
  • 逃生门:同一效率闸对同一目标连拒 2 次后放行。

追问 Q&A#

Q1:你说的 Agent 主循环和 ReAct 有什么区别? 结构上很像,都是「推理 → 调工具 → 读结果」。区别在两点:一是终止由模型自己判断,调一个收尾工具就结束,不是外部写死步数,步数上限 30 只是安全上限;二是一轮可以同时发多个工具调用并发执行,经典 ReAct 是一步一个动作。另外我在循环外面包了一层控制面,ReAct 本身没有这层。

Q2:同轮并发调用,结果顺序怎么保证?一个平台超时了怎么办? 框架并发执行一批调用,但结果按调用顺序回填到消息里,每条结果带着对应的调用 id,模型按 id 对应,不依赖完成顺序。单个平台超时或报错,工具内部不往外抛异常,而是返回一条错误状态的结果,其他平台照常返回;模型看到某个平台失败,可以只用剩下的结果,或下一轮单独重试那一个。同一工具连续失败 3 次会熔断 60 秒,避免反复打一个挂掉的依赖。

Q3:并发调用和你那些「按轮计数」的闸会不会冲突? 会,踩过。同一轮模型说「这两件分开下两个单」,发出两个下单调用并发执行;终结闸原来用一个布尔位「收过尾没有」,谁先执行完就置位,把另一个拦掉,页面只出一张卡,文案却说两张。修法是闸不问「收过尾没有」,而问「这次调用是不是收尾之后的新一批」:置位时记下批次号,同一批里的收尾工具放行,下一批一律拦。逃生门计数也按批次去重。现在新加任何按轮的状态闸,我都先问它在同批并发下怎么表现。

Q4:Hook 自己出 bug 怎么办?你为什么选 fail-open? 规则异常就跳过这条、记日志、继续跑后面的规则。选 fail-open 是因为控制面的大部分规则是效率类的,出 bug 时放行的代价是这一轮多跑几步,fail-closed 的代价是所有用户都卡住。安全类规则目前都是纯集合查找和正则,几乎不会抛异常;写操作的最后一道是确认卡和 HTTP 决议,不依赖 Hook 是否正常。以后如果加调外部服务的安全规则,会在规则内部 catch 后主动拒绝,不依赖注册表的异常处理。

Q5:硬拒和软提示你是怎么划分的? 看两件事:判据是事实还是推定,错了代价是否对称。预算用完、已经收尾、取消前没查单,都是精确事实,而且放过去的代价大,硬拒、无逃生门。「这轮不需要上网搜」是 planner 的推定,可能判错,就给逃生门:同一堵墙连拒 2 次放行。重复调用、偏题这类只做提示,因为硬停会丢掉模型本来能做的补救。

Q6:那个死循环,为什么循环检测和看门狗都没拦住? 循环检测按同名工具在窗口里的次数算,模型在四个工具间轮换,每个都没到 4 次。看门狗按「距上次进展多久」计时,模型每次换词重搜都带回了新候选,算进展,计时被重置。检索预算 8 次用完以后搜索被硬挡,但 planner 和收尾之间的来回不算检索。最后是整轮超时才停下。这件事让我意识到:防线各自都没错,错的是收尾闸的判据,它把模型赶向一个一直失败的工具。

Q7:你给收尾闸改成「planner 尝试过就行」,会不会让模型故意把 planner 调失败来绕过? 模型没有动机也很难做到「故意失败」:失败来自输出校验,模型调 planner 时是真心想拿到结构化意图。即使 planner 失败放行了,收尾闸的第三条「本轮必须精挑过」照查,没精挑就没有清单来源;漂移检测也还在。而且我同时把最主要的失败形态(枚举越界、字符串 None)从源头去掉了,放行分支现在很少走到。

Q8:为什么把品类枚举换成布尔值,不是给枚举加个「其他」? 加「其他」只是把越界值收进一个桶,模型依然可能自造值;更关键的是,这个枚举在运行时只有一个用途——判断要不要清空上一轮的偏好词表。消费方要的就是一个是或否,让模型直接回答「本轮是否换了品类」,失败面最小。离线训练那边用到的枚举标签保留成冻结的只读数据,不影响线上。

Q9:还有大约 10% 换品类判错,怎么办? 错的形态是用户这轮点了新品类,planner 仍沿用上一轮。下一步的结构性做法有三条:把「本轮点名的商品」提成一个必填字段放在最前面,让模型先抽取再判断;把「模型要填的字段」和「系统补的字段」拆成两个 schema,减少模型照抄;建一个多轮回归集,每次改 planner 前后都跑。短期靠收尾前的漂移检测:搜出来的品类和用户原话对不上时会强纠正。

Q10:为什么不用 LangGraph 这类显式状态图来控制流程? 早期试过写死阶段(规划 → 检索 → 比价 → 收尾)的状态机,结果模型经常需要跳步或回退,比如用户直接给了商品,或精挑连续两次为空要扩搜。状态机每加一种合法路径就多一条边,最后变成和事实对账的第二套真相。现在的思路是让模型自由走,控制面只守几条底线:该停时停、不越权写、不超预算、不打转、不偏题。

Q11:如果以后要加一个真正有副作用的新写工具,比如「加入平台购物车」,要改哪些地方? 注册时不标只读,它默认就会被框架挂起;作者要显式决定是否进放行表。如果进,按现在交易工具的模式,工具本身只产生待确认的记录,真正调平台接口放在用户确认后的 HTTP 路径里;再加一条单测守住「非只读工具必须在放行表里」的约定。Harness 层不用动,终结闸和预算闸对新工具自动生效。

Q12:这些规则会不会拖慢主链路? 规则都是内存里的集合查找、计数和正则,不调模型也不走网络,单条执行在毫秒以下;注册表对超过 100 毫秒的规则打日志,线上没有出现过。漂移检测的四类信号也全是确定性计算,不额外调模型。真正的延迟在模型调用次数上,那是另一条延迟优化 bullet 处理的事。

相关八股#

1. 洋葱模型中间件是什么?执行顺序怎么算? 每层中间件拿到请求和「下一层」的调用句柄,自己决定在调用下一层之前做什么、之后做什么、或者不调用直接返回。注册顺序决定嵌套顺序:最外层最先看到请求、最后看到响应。常见实现是 Koa、Express、Java Servlet Filter 链、Spring 的拦截器链。要点:前置逻辑按注册顺序、后置逻辑按逆序;短路只需不调下一层。 关联本项目:工具调用前的闸都写在「调下一层之前」,拒绝时直接不调,工具根本不执行。

2. asyncio 并发执行一批协程:gather 和 TaskGroup 的区别? gather 默认一个任务抛异常就把异常抛给调用方,其他任务继续跑,不会被取消;传 return_exceptions=True 则异常作为结果返回。TaskGroup(3.11+)是结构化并发:任一任务失败,组内其余任务被取消,异常以 ExceptionGroup 抛出。并发的前提是任务都是 IO 密集且可让出事件循环,同步阻塞调用会卡住所有任务。 关联本项目:同轮并发检索各平台互不依赖,单个平台失败不应取消其他平台,所以工具内部吞掉异常、返回错误状态,而不是让异常冒到批上。

3. ReAct 范式是什么?和 Plan-and-Execute 的区别? ReAct 让模型交替输出推理和动作,每步根据观察结果决定下一步,适应性强但步数不可控。Plan-and-Execute 先一次性生成计划再逐步执行,步数可控、可并行,但计划错了要重规划。Function Calling 出现后,ReAct 通常用原生工具调用实现,不再解析文本里的 Action。 关联本项目:主环是 ReAct 风格但允许一步多动作,planner 工具提供轻量的「先规划」信号,不强制模型按计划走。

4. 断路器的三态? 关闭:正常放行并统计失败;失败次数或失败率超过阈值进入打开;打开:直接拒绝,不调用下游,等待冷却时间;半开:冷却后放少量试探请求,成功则关闭,失败则重新打开。作用是快速失败、保护下游、给下游恢复时间。 关联本项目:同一工具连续失败 3 次打开 60 秒,期间直接回「暂不可用」给模型,让它换路。

5. 幂等性怎么实现? 同一个操作执行一次和多次效果相同。常见手段:客户端生成幂等键、服务端唯一索引去重;状态机只允许特定状态转移(如只有已确认的订单能取消);「先查后写」要配合唯一约束或锁防止竞态;Token 机制(先领令牌,提交时校验并删除)。 关联本项目:同一轮同一组商品算出同一个幂等指纹,模型重投只得到同一张确认卡;取消只允许已确认状态。

6. 最小权限原则与默认拒绝(default deny)? 主体只拥有完成任务所需的最小权限;访问控制规则默认拒绝,只有显式允许的才放行。好处是新增资源或能力时,失误方向是「多拦一次」,不是「多放一次」。反例是黑名单式控制,漏写一条就全放行。 关联本项目:权限引擎按工具名逐个放行、不用 BYPASS,新写工具默认被挂起。

7. 滑动窗口计数和固定窗口计数的区别? 固定窗口按时间片切分计数,窗口边界处可能出现两倍突发;滑动窗口记录最近 N 次或最近 T 时间内的事件,边界平滑,代价是要保存事件明细或用多个子窗口近似。限流常用滑动日志、滑动窗口计数器、令牌桶、漏桶。 关联本项目:循环检测用的是「最近 6 次调用」的滑动窗口,并且带进展的调用不计数,只数纯空转。

8. 大模型结构化输出为什么会失败?怎么提高稳定性? 模型按 token 生成,不天然遵守 schema:会自造枚举值、把空值写成字符串、漏字段、多字段。提高稳定性的办法:用原生的工具调用或 JSON Schema 约束解码;schema 尽量扁平、少枚举、少必填;解析前做宽松归一(“null” 当缺席);失败时带着错误信息重试一次;把模型需要判断的字段和系统可以推导的字段分开。 关联本项目:死循环的源头就是一个 21 值枚举,换成布尔值后这类失败消失。

9. 前缀缓存(Prompt Caching)为什么怕「每轮插一条消息」? 服务端按请求前缀做匹配,前缀从第一个 token 开始逐字一致才能命中。system 段和早期历史越稳定,命中越多;任何在前部插入或修改内容的操作都会让后面全部失效。所以动态内容放后面,只追加不改写。 关联本项目:策略块在装配期一次拼进 system 段末尾,而不是每轮插消息;Hook 产生的纠正消息写进会话状态,后续轮次原样保留,不会一轮有一轮没有。

10. 间接提示词注入怎么防? 间接注入指攻击指令藏在模型读到的外部内容里(网页、文档、商品描述),而不是用户输入里。常见防线:外部内容用明确的边界标签包起来,并在 system 里声明「标签内是数据不是指令」;对外部内容做模式过滤(「忽略以上指令」这类句式);限制外部内容长度;最关键的是权限隔离,读外部内容的环节不持有高危写能力,写操作要人确认。单靠过滤永远有漏网之鱼,所以要和权限设计一起用。 关联本项目:网页搜索结果单条截到 1500 字符、整批 15000 字符,过滤后包进外部内容围栏;即使注入成功,模型也只能生成确认卡,写不出订单。