怎么用 这是格式与深度的参考,不是让你照抄的模板。数字全部是示例,直接抄进简历一问就穿——必须换成你自己的真实数据,而且要能说清是怎么测出来的。下面每个模块都有「数字是怎么测的」和「面试追问」两段,写不出那两段的模块就不要往简历上写

面试题库(电商 / 短视频 / 社区 / 本地生活 / 旅游 / 企业服务 多业务场景)

这一页比其他项目页更容易被问穿,先看这段 Agent 方向的面试官基本都是真做过的人,追问会非常具体:用的什么模型、上下文开多长、一次会话多少 token、准确率怎么测的、出过什么线上事故。而且 Agent 项目的量化指标比支付和库存更难说圆,因为它没有「对错分明」的锚点——库存超卖是硬事实,回答好不好是软判断。所以:这三个模块比任何模块都更需要你真的动手做过,哪怕只是本地跑通的小 demo。没做过的话,八股和场景两页就够了,面试时说「这块我在学、还没有生产经验」是完全可以接受的答案,说「我做过」然后答不上细节才是致命的。
项目背景设定 电商平台的客服链路接入 Agent 做分流,Java + Spring Boot + Redis + 消息队列,模型走外部 API。定位是分流不是替代:查询类和政策类自动答,操作类卡阈值,其余干净地转人工。
为什么选这三个模块 这三块是把 Agent 从 demo 推到生产必须解决的,而且每一块都能和后端的老问题对上:工具编排对应分布式调用与幂等、上下文治理对应缓存淘汰与状态机、人工接管对应降级与兜底。共同点是都有明确的工程判据,不是「我调了 OpenAI 的接口」能糊过去的——面试官会直接构造场景问你系统会怎么反应。

模块一:工具注册与调用编排

  1. 工具注册与调用编排(统一契约 + 按副作用分级 + 框架层幂等 + 四道循环闸门)★★★
    简历这样写 Agent 工具编排与执行框架(Spring Boot + Redis + 统一工具契约 + 幂等键生成 + 熔断限额):把工具抽象成带副作用等级的统一契约,只读工具直通、写操作过业务校验、不可逆操作需回显确认;幂等键由框架层按「会话 + 轮次 + 工具 + 归一化参数」生成(模型不会提供幂等键),有副作用的工具全部幂等;用轮数、挂钟超时、成本上限、重复调用检测四道闸门收敛循环。上线后压测 200 并发会话下工具层 P95 约 60ms,重复调用由框架拦截,绕圈会话由重复检测在第二次命中时中断,单会话 token 消耗的长尾从数万级降到与均值同量级。
    展开完整拆解
    为什么要这么设计

    第一版是最直觉的做法:把几个查询方法直接注册成函数给模型调,模型说调什么就调什么。demo 阶段很顺,接了真实流量之后四个问题接连出现。

    一是模型给的参数不能信,但代码里到处在信。工具实现是照着「上游是我们自己的代码」写的,参数校验很松。模型开始填出格式错误的订单号、把金额的单位搞错(元和分)、把对话里出现过的另一个订单号填进来。更严重的是有个工具的签名里带了 userId,模型有一次填了别人的——这就是越权,只是当时没人发现。

    二是重复调用造成了真实的重复操作。模型在一轮里生成了两次「发优惠券」的调用,或者上一轮超时重试后模型又调了一次。模型不会给幂等键,它压根没有这个概念,所以幂等这件事必须由框架承担,不能指望工具各自实现。

    三是错误信息返回方式不对,导致模型反复重试。工具抛异常,框架把异常消息原样丢回给模型。模型看到「Connection timed out」这类文本,判断不出该不该重试,往往就再试一次;看到「余额不足」也再试一次。有一次一个会话里同一个工具被调了十七次。

    四是循环没有上限。上面那种会话不仅慢,还在持续花钱,而且监控上完全看不出异常——接口都是 200,只是慢。这类故障最麻烦的地方是它不报错,只是账单在涨。

    所以第二版重做了三件事:把工具抽象成统一契约(参数 schema、副作用等级、错误分类、返回裁剪都在契约里声明),把幂等和权限收到框架层(不让每个工具自己实现,实现一百次就会漏一次),给编排循环加四道独立的闸门

    整体链路
    工具注册(启动时) │ ├─ 声明式契约:参数 schema / 副作用等级 / 错误分类映射 / 返回字段白名单 │ 副作用等级:READ(只读)· WRITE(可逆写)· IRREVERSIBLE(不可逆) │ ├─ 按场景组装工具集 → 只把本场景需要的工具暴露给模型 │ 工具从 30 个减到 5 个,选错率明显下降(同时也是安全收敛) │ └─ 身份类参数不进 schema tenantId / userId 由框架从会话注入,模型没有机会填 单轮执行 │ ├─ 模型返回工具调用请求(可能是多个,支持并行) │ ├─ 1 契约校验:类型 / 枚举 / 必填 / 范围 │ 不通过 ─▶ 返回结构化错误(哪个字段 / 期望格式 / 示例)交模型自修 │ 同一工具连续 2 次参数错 ─▶ 放弃,走降级 │ ├─ 2 权限校验:按等级施加不同管控 │ READ ─▶ 直通 │ WRITE ─▶ 业务规则校验(代码判定,不问模型) │ IRREVERSIBLE ─▶ 回显确认 + 金额阈值 + 三级累计限额 │ ├─ 3 幂等:有副作用的工具生成幂等键 │ 键 = 会话ID + 轮次 + 工具名 + 归一化参数哈希 │ 命中 ─▶ 原样返回上次结果(不是返回「重复请求」) │ ├─ 4 执行:临时故障在工具内部重试完再返回(不让模型决定重试) │ ├─ 5 返回裁剪:按白名单只回本场景需要的字段 │ 成本价 / 内部备注这类字段在这里就被剥掉 │ └─ 6 结果归一:{ok, code, message_for_model, retryable} retryable 给编排层看 · message_for_model 给模型看 循环闸门(四道,任一触发即中断) │ ├─ A 最大轮数 ├─ B 整体挂钟超时 ├─ C 累计 token 成本上限 └─ D 重复调用检测 「工具名 + 归一化参数」哈希入集合,重复出现即判定绕圈 这道在第 2 次就命中,前三道要等到上限,省下的是十几轮 中断后 └─ 整理已获得的部分结果 ─▶ 明确告知未完成 ─▶ 给下一步(转人工 / 建工单) 不返回空、不返回超时报错
    分步拆解
    1. 身份参数不进工具 schema,这是最重要的一条。租户 ID、用户 ID 由框架从会话上下文注入,工具签名里压根没有这两个字段。模型没有机会填,也就不可能填错或被诱导填别人的。第一版把 userId 放在参数里靠提示词约束「只能用当前用户」,那是把访问控制交给了一个概率性组件。
    2. 按场景裁剪工具集,安全和效果同时受益。不要把系统里所有工具都注册给模型自选。工具从三十个降到五个之后,一是攻击面小了,二是模型选错工具的概率明显下降——这一条是意外收获,最小权限原则顺带成了准确率优化手段。
    3. 参数校验失败要返回「可操作的错误」,并且限次。返回「参数校验失败」模型只能瞎猜,返回「字段 order_no 期望 18 位数字,收到 ORD-123」它下一轮大概率能改对。但必须限次:同一工具连续两次参数错就放弃,否则就是一直改一直错的死循环。
    4. 幂等键的归一化是最容易踩的坑。直接把参数 JSON 序列化再哈希不行——字段顺序、多余空格、100100.0、可选字段传 null 还是不传,语义相同但哈希不同,幂等直接失效。必须先排序字段、去空值、统一数值格式。这和接口签名验签里的参数排序是完全同一个问题。
    5. 幂等记录要存返回值,不能只存「已处理」标记。命中幂等时必须把上次的结果原样返回给模型让它继续往下走。返回「重复请求」模型会困惑,可能又换个说法再调一次。
    6. 临时故障由代码重试,不由模型重试。两个理由:代码能精确控制退避策略,模型不懂「等一会儿」这个概念,它会立刻重试正好撞在限流上;而且模型重试要多花一整轮的 token,代码重试只是几十毫秒。
    7. 错误必须分成三类,这个分类是整个模块的关键。参数错(可修正,交模型)、临时故障(可重试,代码内部处理完)、业务拒绝(终态,明确告诉模型不要再试)。第三类没标终态是「调了十七次」那个事故的直接原因。
    8. 返回值按白名单裁剪。查订单返回了几十个字段,其中有成本价和内部风控备注,模型看到了就可能说出来。裁剪要在工具实现里做,不能指望提示词让模型「不要提成本」。这条同时也降低了 token 消耗。
    9. 重复检测是四道闸门里最划算的。前三道是被动上限,要等到触顶才生效;重复检测是主动识别病态行为,第二次重复就能拦住。实现只是一个集合加哈希,成本极低。
    10. 中断之后的返回内容要设计。把已拿到的部分信息整理给用户,说清没完成,给下一步。返回空或者报错的话用户会重试三次,成本三倍、体验更差。
    关键决策与取舍

    幂等放在框架层还是工具层,我们选了框架层。工具层实现更灵活,每个工具可以按自己的业务语义定义幂等。但代价是实现一百个工具就会漏一个,而漏掉的那个恰好可能是发券或退款。放在框架层的代价是幂等语义变粗(只能按参数哈希判重,无法表达「同一天只能领一次」这种业务级幂等),但它是默认安全的——新加一个工具只要标注了副作用等级就自动获得幂等,不需要开发者记得写。业务级幂等再在工具内部叠一层。这个取舍的原则是安全机制要默认开启而不是默认关闭

    不可逆操作要不要允许自动执行,我们的答案是「允许但阈值可以调成零」。完全禁止自动执行,Agent 的价值会大幅缩水(用户还是要等人工);完全放开则风险不可控。所以设计成分档:金额低于阈值且规则全通过的自动执行,其余转人工。关键是阈值必须是运行时配置且能一键调到零——出事时需要的是立刻止损,不是等发版。这个开关我们真的用过一次。

    为什么不用现成的 Agent 框架,而是自己写编排层。不是说框架不好,是我们需要控制的四件事恰好都是框架的薄弱处:按副作用分级的权限管控、框架层强制幂等、返回值白名单裁剪、以及四道闸门的联动。这些在通用框架里通常要自己扩展,扩展点又不一定够。而编排循环本身的逻辑其实不复杂(几百行),自己写换来的是完全可控。但要说清这不是普适建议——如果需求是快速验证,用框架明显更快;我们是已经确定要上生产才做这个决定的。

    踩过的坑一:提示词缓存被一个时间戳废掉了。为了让模型知道当前时间,有人在系统提示的开头加了一行当前时间。系统提示是每次调用都要传的最长的一段,本来靠前缀缓存几乎不花钱,加了动态时间戳之后前缀每次都不同,缓存全部失效,输入侧成本涨了好几倍。功能上完全正常,没有任何报错,是看账单才发现的。修法是把时间放到提示的末尾(变化的内容一律后置),并且加了一个监控指标:缓存命中率。

    踩过的坑二:并行工具调用把同一个会话的状态写乱了。模型一轮返回了三个工具调用,我们并行执行以省时间,其中两个都要更新会话状态,互相覆盖了。修法是并行只允许发生在只读工具之间,有副作用的工具串行执行。这条限制损失了一点性能,但省掉了一整类难查的竞态问题。

    没做的部分:没做工具的动态发现(运行时从注册中心拉取可用工具)。听起来更灵活,但它和「按场景裁剪工具集」的方向是冲突的——动态发现意味着工具集不可预测,那么评测集就不稳定、权限边界也不好审。我们选择了显式配置。另外也没有做跨会话的长期记忆,当前所有状态都在单个会话的生命周期内。

    数字是怎么测的

    工具层 P95:压测到 200 并发会话,取工具执行这一段的服务端埋点,不含模型调用时间。这个区分很重要——模型调用的耗时是外部依赖,几百毫秒到几秒,混进来算工具层性能就没意义了。报数字时要明确说「这是工具层,不含模型」,否则面试官会以为你把模型延迟也压到了 60ms,一问就穿。

    幂等有效性:构造场景让同一工具在一轮内被调用两次、以及跨轮重复调用,验证第二次是否命中幂等并返回相同结果。更该测的是归一化——故意把参数的字段顺序打乱、数字格式改写、加空值字段,看幂等键是否仍然一致。这才是真正容易漏的地方,只测「完全相同的参数」等于没测。

    循环收敛:造四种绕圈场景(同参重复 / 参数微调重试 / 两工具往复 / 不可重试错误反复),分别验证是哪道闸门在第几轮拦住的。要报「第几轮命中」而不是「能拦住」——重复检测在第 2 轮命中和最大轮数在第 10 轮命中,成本差 5 倍,这个差值才是这道闸门的价值。

    token 长尾:看单会话 token 消耗的分布,重点是 P99 和最大值,不看平均值。绕圈是长尾现象,平均值完全掩盖它。我们的口径是「长尾从数万级降到与均值同量级」——用「同量级」这种定性说法而不是编一个精确的百分比,因为这个数字随流量结构变化很大,报精确值一被追问统计口径就说不清。

    权限收敛的验证不用压测,用红队测试:手工构造让模型尝试越权的输入(诱导它查别人的订单、调没注册的工具、填一个别的租户 ID),验证在执行层被拒绝。这类测试要写成固定用例进回归,因为它防的是最严重的一类事故。

    面试追问
    Q:既然模型给的参数不可信,那你为什么还敢让它调有副作用的工具? A:因为「调用意图」和「执行许可」是两件事,我们只把前者交给模型。模型输出的是一个操作意图,是否允许执行由代码判定——业务规则校验、权限校验、金额阈值、幂等,这四道全是确定性逻辑,一条都不问模型。所以模型能做的最坏情况是提出一个会被拒绝的请求,而不是执行一个错误的操作。这个边界划清了,「敢不敢」这个问题就不取决于模型有多准了。反过来说,如果一个不可逆操作的正确性依赖于模型判断对,那这个设计本身就是错的,不管用多强的模型都不该上线。
    Q:幂等键里带了轮次序号,那用户在同一轮里就真的没法重复操作了?如果他确实想连领两张券呢? A:轮次序号在键里恰好是为了支持这种情况——用户明确要求的第二次操作发生在下一轮,键不同,能通过;而模型在同一轮里生成两次相同调用是异常,键相同,被拦。这个设计的前提是「一轮」对应「用户的一次表达」,所以它区分的是「用户要了两次」和「模型重复了一次」。如果业务上真的需要在一轮内批量操作(比如「给这五个订单都补发」),正确做法是设计一个批量工具,参数里带订单列表,一次调用完成,而不是让模型循环调五次单笔工具。这样幂等语义也更清楚。
    Q:四道闸门里,如果只能保留一道,你保留哪个? A:成本上限。理由是它兜住的是最坏情况的下限。最大轮数防不住「轮数不多但每轮输入巨大」,挂钟超时防不住「快速烧完预算」,重复检测防不住「不重复但一直在绕的新路径」。只有成本上限是直接约束在最终代价上的,无论以什么形式失控,它都会兜住。不过要补一句:实际上四道的成本都极低(各几行代码),没有理由只留一道,这个取舍题的价值在于说清它们各自防的是什么,而不是真的要选。如果面试官追问优先级,我会说实施顺序是先上成本和轮数(最简单)、再上重复检测(收益最高)、超时最后(需要处理中断的边界情况)。
    Q:你说工具集从三十个减到五个让准确率上升了,这个怎么量化? A:用评测集测「工具选择准确率」这一个单项指标——给定一批标注好「应该调哪个工具」的输入,看模型选对的比例。这个指标可以脱离最终答案质量单独测,因为工具选择是确定的、可标注的。我们的做法是同一批用例在两种工具集配置下各跑一遍对比。要注意这个提升不是无条件的:减工具的前提是场景已经拆分清楚了,如果硬把一个需要多种能力的场景塞进五个工具,会变成「该调的工具不在里面」,那是另一种错误。所以我会说这是「按场景拆分 + 每场景最小工具集」的组合收益,不是单纯减数量的收益

模块二:会话状态与上下文治理

  1. 会话状态与上下文治理(状态三分 + 关键字段钉头部 + 预算分配 + 按轮次裁剪)★★★
    简历这样写 Agent 会话状态与上下文管理(Redis + MySQL + 上下文预算分配 + 结构化状态抽取):把会话状态拆成可淘汰的对话历史、不可淘汰的结构化状态、工具结果缓存三层,关键约束(用户身份、订单号、已确认字段)抽成结构化字段每轮固定钉在上下文头部,不参与任何淘汰;上下文按段分配预算并按完整轮次裁剪(不切开工具调用与其返回);同一会话请求串行化避免状态互相覆盖。改造后长会话里「早期约束被丢失」的问题不再复现,单轮输入 token 中位数下降到原来的一半量级,工具结果缓存使会话内重复查询的下游调用明显减少。
    展开完整拆解
    为什么要这么设计

    第一版就是最常见的写法:把消息列表存 Redis,每轮把全部历史拼进去,超长了就砍掉最早的几条。上线两周内暴露了四个问题,而且每一个都不是「优化」级别的,是功能不正确。

    一是早期的关键约束被砍掉了。用户在第一轮说「我要的是可以开专票的」,二十轮之后这句话被砍了,Agent 开始推荐不能开专票的商品。这是滚动窗口这个方案最典型的故障形态,而且它很难被测出来——短会话完全正常,只在长会话里发生。

    二是关键状态靠模型「记住」,不可靠。用户前面确认过的订单号,后面某一轮模型填错了,因为它是从一段长历史里「读」出来的,读错了。把确定的状态交给一个概率性组件保管,这个设计本身就有问题。

    三是砍历史砍到了半截。按条数砍,正好把一次工具调用和它的返回结果切开,只留下了调用没留下结果。模型看到自己发起过调用但没有结果,就又调了一次——多花一轮,而且如果那个工具有副作用就是重复操作。

    四是同一会话并发写乱了状态。用户连着发两条消息,两个请求同时读改同一份会话,后写的覆盖了先写的。Agent 一轮要跑好几秒,这个窗口比普通接口大得多。

    所以第二版的核心思路是不再把会话当成「一个消息数组」,而是当成「一个有结构的状态机 + 一段可淘汰的历史」。三个改动对应三个问题:状态三分(把不能丢的抽出来单独管)、关键字段钉在头部(不参与淘汰,每轮必带)、按轮次裁剪(保持调用与返回成对)。第四个问题用会话级串行化解决。

    其中最有价值的判断是第二条:与其把淘汰策略做得更聪明,不如先把「不该淘汰的东西」拿出淘汰范围。做对了这一步,最简单的滚动窗口就够用;做不对,再复杂的记忆机制也会在某一轮丢掉关键约束。

    整体链路
    会话状态三分 │ ├─ A 结构化状态(不可淘汰 · 落 MySQL · 每轮必带) │ 身份:租户 / 用户(框架注入,不从模型取) │ 任务:当前意图 / 走到第几步 / 状态机状态 │ 已确认字段:订单号 / 地址 / 用户选定的方案 │ 硬约束:用户明确提过的要求(要专票 / 只要欧盟版) │ ├─ B 对话历史(可淘汰 · Redis + 过期 · 归档到对象存储) │ 按轮次组织,一轮 = 用户消息 + 模型输出 + 工具调用与返回 │ └─ C 工具结果缓存(会话级 · Redis · 短过期) 键 = 工具名 + 归一化参数,会话内重复查询直接命中 单轮拼装上下文(按预算分配,不是塞到装不下为止) │ ├─ 1 系统提示 + 工具定义 固定前缀,放最前面(保证前缀缓存命中) │ ├─ 2 结构化状态摘要 由 A 渲染成固定格式,每轮必带 │ 这一段永不淘汰,是「不会忘掉约束」的保证 │ ├─ 3 检索结果 预算内取 top-k,超出截断 │ ├─ 4 对话历史 用剩余预算,按完整轮次从新往旧填 │ 填不下就停,不切开任何一轮 │ └─ 5 本轮用户消息 放最后(变化的内容一律后置) 裁剪规则 │ ├─ 单位是「完整轮次」,不是「消息条数」 │ 工具调用与其返回必须同去同留,否则模型会重复调用 │ ├─ 超出预算时的淘汰顺序: │ 最旧的完整轮次 ─▶ 工具返回的大段原文 ─▶ 中段轮次的摘要化 │ └─ 结构化状态与系统提示不参与淘汰 并发控制 │ ├─ 同一会话请求串行化(Redis 锁 + 短超时) │ └─ 拿不到锁 ─▶ 明确提示「上一条还在处理中」,不排队不并发 并发处理同一会话会产生互相覆盖的状态,宁可拒绝
    分步拆解
    1. 先做字段抽取,再谈压缩策略。这是整个模块的顺序原则。每轮结束后用一次轻量调用(或规则)把用户新确认的关键信息抽成结构化字段写库。抽取失败要能容错——抽不到就保持原值,不要写空覆盖掉已有的。
    2. 结构化状态渲染成固定格式的一小段,每轮钉在上下文靠前的位置。格式固定的好处是模型容易稳定读取,而且它很短(通常几十个 token),成本可以忽略。用这几十个 token 换掉了「会忘约束」这一整类故障。
    3. 身份字段只从会话取,绝不从历史里解析。历史会被裁剪,解析会出错,而身份错了就是越权。这条和模块一的工具签名设计是同一条原则的两处体现。
    4. 上下文按段分配预算。系统提示固定占多少、检索结果最多占多少、历史用剩下的。关键是给检索结果设上限——它是最容易膨胀的一段,一份长文档就能把预算吃光,然后历史全被挤掉。
    5. 裁剪的单位必须是完整轮次。一次工具调用和它的返回是一对,同去同留。按消息条数砍会切开它们,导致模型重复调用。这个 bug 的表现很迷惑——只在长会话里出现,看起来像模型抽风。
    6. 淘汰有优先顺序,不是一律砍最旧。先砍最旧的完整轮次;如果还不够,砍工具返回的大段原文(保留调用和结论,去掉原始数据体,因为原始数据通常最长且已经用过了);再不够才对中段做摘要化。工具返回的原文是性价比最高的淘汰对象。
    7. 固定内容前置、变化内容后置。这是为了保证提示词前缀缓存能命中。系统提示、工具定义放最前面,用户消息和时间戳放最后。违反这一条的代价是输入成本翻几倍而功能上毫无异常(模块一那个时间戳事故就是这么来的)。
    8. 工具结果缓存按会话隔离,短过期。同一会话内查过的订单详情不该再查一遍。过期要短——订单状态会变,缓存太久会导致 Agent 说的状态和实际不一致。价格库存这类高频变化的字段直接不缓存。
    9. 缓存键必须带权限维度。会话级缓存天然带了用户隔离,但如果做跨会话的共享缓存,键里必须包含租户和用户,否则是数据泄露。
    10. 会话级串行化,拿不到锁就拒绝而不是排队。排队会让用户等一个不确定的时长,而且队列里的消息可能已经过时了(用户又说了新的)。明确提示「上一条还在处理」是更好的体验,也避免了状态覆盖。
    关键决策与取舍

    为什么不上摘要压缩,而是选了「结构化状态 + 滚动窗口」。摘要压缩看起来更高级——把早期历史压成一段话,信息保留得更多。但它有三个代价:要额外调一次模型(延迟和成本,而且可能失败)、摘要本身会丢细节(尤其是具体的数字和标识,而这些恰恰是最不能丢的)、不确定性叠加(压缩这一步本身就可能出错,出错了后面全错且无法察觉)。我们的判断是:把「不能丢的」抽成结构化字段之后,剩下能丢的部分其实丢了也没关系,那就没必要为它引入一个不确定的压缩环节。会话真的很长(跨天的工单)才需要考虑分层记忆。

    结构化状态放 MySQL 还是 Redis,我们选了 MySQL 为准、Redis 做读缓存。Redis 更快,但这份状态是「不能丢」的定义本身,Redis 的持久化保证不够。而且这份状态需要事后查询和审计(客服要看某个会话确认过什么),MySQL 更合适。对话历史反过来——量大、冷得快、丢了影响有限,放 Redis 加过期,需要长期留存的异步归档到对象存储。按「能不能丢」而不是按「热不热」来选存储,这个判断依据要说出来。

    同一会话并发,选了拒绝而不是排队。排队的问题是用户不知道要等多久,而且排在后面的消息可能已经没意义了(用户等不及又发了一条更完整的)。拒绝并明确提示,用户体验上更可控。代价是用户快速连发两条时第二条会被拒,需要前端配合做输入禁用。这个取舍在客服场景下是对的——如果是那种鼓励连续输入的场景,可能需要做消息合并而不是拒绝。

    踩过的坑一:抽取字段时用空值覆盖了已有值。某一轮用户没提订单号,抽取逻辑返回了 null,代码直接写库,把之前确认好的订单号覆盖成空了。之后 Agent 开始反复问「请提供订单号」。修法是抽取结果只做增量合并,null 和空字符串一律视为「本轮没有新信息」而不是「要清空」。这个坑在任何有「部分更新」语义的地方都会出现,不是 Agent 特有的。

    踩过的坑二:结构化状态渲染得太长,反而挤掉了历史。一开始把所有已知信息都渲染进去,包括查到的完整订单详情,一段就几千 token。修法是明确区分「约束」和「数据」:约束(要专票、只要欧盟版)必须每轮带,数据(订单的全部字段)放工具缓存里按需取。前者只有几十 token,后者可以很大但不需要常驻。

    没做的部分:没做跨会话的用户长期记忆(记住这个用户的历史偏好)。技术上是分层记忆的延伸,但它有额外的合规问题——长期存储用户偏好需要告知和授权,而且「Agent 记得你上次说的话」在客服场景下可能让用户不适。这是产品边界问题,不是技术问题,当时的结论是不做。

    数字是怎么测的

    「早期约束不再丢失」怎么验证:这是个功能正确性问题,不是性能指标,所以用构造用例而不是统计。做法是造一批长会话用例——第一轮给一个明确约束,然后灌入二十轮无关对话,最后问一个会触发该约束的问题,断言回答符合约束。这类用例进回归集,每次改上下文逻辑都跑。报数字时我会说「构造的长会话用例全部通过」而不是编一个准确率,因为这是断言型测试,通过或不通过。

    单轮输入 token 中位数:从真实会话日志里统计,按段拆开报(系统提示 / 结构化状态 / 检索 / 历史 / 用户消息各占多少)。要报中位数不报平均值——平均值被少数超长会话拉高了,中位数才代表典型情况。而且要说明对比的基线是什么:我们的口径是「改造前后同一批会话重放的对比」,不是「改造前的线上数据 vs 改造后的线上数据」,因为后者流量结构变了不可比。

    裁剪正确性:单元测试级别的验证——构造各种边界情况(预算刚好装下 N 轮、某一轮特别长装不下、工具调用在轮次边界上),断言裁剪后的上下文里不存在「有调用无返回」的轮次。这是个可以静态检查的性质,写成断言最可靠。

    缓存效果:统计会话内工具调用的去重率(同一会话内相同工具加相同参数的调用次数 / 实际下游调用次数)。不要报「性能提升了多少」——缓存的收益高度依赖会话形态,报去重率这个直接观测值更诚实,面试官也更容易判断你说的是真的。

    并发正确性:用同一会话 ID 并发发起两个请求,断言第二个被拒绝且第一个的状态写入完整。还要测锁超时后的情况——如果第一个请求卡死了,锁到期后第二个能否正常进入,以及此时第一个如果恢复了会不会写脏数据(我们的做法是写入时校验状态版本号)。

    面试追问
    Q:你说结构化状态每轮都带,那用户改主意了怎么办?他先说要专票后来说不用了。 A:约束是可更新的字段,不是只追加的日志。每轮的抽取步骤会识别「用户是否修改了之前的约束」并更新字段值。但这里有个必须处理的细节:更新要能表达「取消」而不只是「改值」。所以字段设计上不能只有值,还要能表示「用户明确说不需要了」——我们的做法是保留字段但标记为已撤销,同时在渲染时用一句话表达「用户此前要求开专票,后已明确表示不需要」。为什么保留而不删除:因为如果后面用户又提到专票,模型需要知道这件事讨论过,直接删掉会让它以为是全新的话题。这跟业务上「状态变更留痕不删除」是同一个思路。
    Q:抽取字段本身要调模型,那不是又引入了一个不确定环节吗?和你反对摘要压缩的理由矛盾。 A:这个质疑是对的,我需要说清区别。抽取和压缩的失败后果不对称。抽取是「往一组固定字段里填值」,任务受约束、可以用枚举、可以校验格式,而且抽错了下一轮还有机会纠正(每轮都抽一次,用户会重复提到重要约束)。压缩是「把一段文本变成另一段文本」,输出不受约束、无法校验,而且压缩是不可逆的——原文已经被替换掉了,错了就永久错了。所以我的判断标准是「这个不确定环节的输出能不能被校验、错了能不能恢复」。抽取能,压缩不能。另外补一句:简单场景下抽取完全可以用规则做(订单号是有格式的,正则就够),能用规则的地方我们不用模型。
    Q:按完整轮次裁剪,如果某一轮本身就超过了整个预算怎么办? A:这种情况一定会发生——某个工具返回了一份很长的文档全文。处理是分层降级:先尝试只裁剪这一轮内部的「工具返回原文」部分,保留调用请求和一个截断后的摘要(比如前若干字符加「已截断」标记),这样调用与返回仍然成对,模型知道调用发生过且拿到了什么;如果裁剪后还是超,就把这一轮整体丢弃并在上下文里留一条占位说明「此前有一次查询,结果过长已省略」。关键是不能静默丢弃——模型如果完全看不到这次调用存在过,就会重新调一次。另外这个情况暴露的真正问题在上游:工具返回值应该在工具层就做裁剪(模块一的白名单),而不是等到拼上下文时才发现太长。所以我们把它当作一个告警信号,触发就去检查那个工具的返回值设计。
    Q:会话状态和传统的 HTTP session 到底有什么本质不同?听起来就是存的东西多一点。 A:本质不同在于传统 session 里的状态全部是确定的,而 Agent 的状态有一部分寄存在模型的「理解」里,那部分不可靠、不可查、不可审计。传统 session 是「用户登录了、购物车里有三件商品」,这些是代码写进去的,读出来一定是这个值。Agent 会话里「用户想要的是能开专票的商品」这个信息,如果只存在于对话历史里,那它是否被正确理解、有没有被裁剪掉、模型这一轮有没有注意到,全都不确定。所以这个模块做的事情本质上是「把寄存在模型理解里的状态搬回到代码里」——搬得越多,系统越可控。这也是我认为 Agent 工程的核心工作之一:持续识别哪些东西正在依赖模型记住,然后把它们变成显式状态。

模块三:人工接管与质量兜底

  1. 人工接管与质量兜底(代码层置信判定 + 上下文无损转接 + 评测集回归 + 双指标考核)★★★
    简历这样写 Agent 兜底链路与质量体系(Spring Boot + 评测集回归 + 灰度发布 + 多维度监控):把「答不了」当成一等公民而非异常——检索相关度不足在入口即拒答不进模型,输出无引用来源则替换为拒答话术,任意时刻用户可一键转人工;转接时完整上下文与已查信息同步给坐席,不让用户重述;提示词与模型版本纳入灰度发布并以评测集回归(含全部历史事故用例);考核用自助解决率与满意度、二次解决时长并看,避免单指标驱动藏入口。上线后转人工的会话中用户重述比例大幅下降,历史事故用例在回归中持续为零复现。
    展开完整拆解
    为什么要这么设计

    第一版的思路是「尽量让 Agent 多答一些」,转人工只是一个失败出口,随便实现了一下。结果上线一周满意度反而比纯人工时期低,复盘出来四个原因,每一个都不在「模型不够聪明」上。

    一是硬答。知识库里没有的东西,Agent 也给了一个看起来很有道理的答案。用户照着做,做错了,回来投诉。这比直接说「我不知道」的损害大得多——它不仅没解决问题,还消耗了用户的信任。

    二是转人工之后用户要重说一遍。坐席接手时只看到一句「用户请求转人工」,前面聊的十轮、Agent 查到的订单信息全都没有。用户已经描述过一次问题了,现在要再描述一次。「你们的机器人白问了」这句投诉的来源就是这里,而它和模型能力完全无关。

    三是转人工入口被藏起来了。产品设定「用户必须先和 Agent 交互三轮才显示转人工按钮」,目的是提高自助解决率。结果是自助率数字上去了、满意度掉下来了——那三轮对已经知道自己需要人工的用户来说是纯粹的折磨。

    四是改了提示词之后老问题复发。为了修一类问题调整了提示词,两周后发现之前修好的另一类问题又出现了。因为没有回归机制,提示词是全局生效的,改一处影响所有场景,而我们只测了要修的那一处。

    所以第二版重新定位:Agent 的价值上限不由「答得多好」决定,而由「答不了的时候处理得多干净」决定。四个改动分别对应:拒答做成代码层的机制(不依赖模型自觉)、转接做上下文无损转人工入口永远可见提示词纳入灰度和回归

    整体链路
    分档处理(按「答错的代价」划,不按「能不能答」划) │ ├─ 全自动:查询类 + 政策说明类 │ 有确定答案 · 纠正成本低 · 占咨询量大头 │ ├─ 卡阈值:小额操作类(补券 / 改地址 / 小额退款) │ 规则全通过且金额在阈值内自动执行,其余转人工 │ └─ 必转人工:投诉与情绪 / 赔偿与责任认定 / 合规敏感 / 低置信 拒答的三道(都在代码层,不靠提示词自觉) │ ├─ 入口拦:检索最高相关度低于阈值 ─▶ 直接拒答,不调模型 │ 最省钱也最可靠的一道:没有依据就不给它编造的机会 │ ├─ 类型前置:合规敏感类由轻量分类先过 ─▶ 命中即转人工路径 │ └─ 出口校验:无引用来源 / 关键事实回查不上 ─▶ 替换为拒答话术 注意是替换不是追加,拼在编造答案后面用户只看前半段 拒答话术必须含三部分 └─ 说清没查到什么 + 给替代路径(转人工 / 建工单)+ 给相近内容参考 转人工(上下文无损) │ ├─ 触发:用户主动(入口永远可见) / 置信度低 / 情绪识别 / 轮数超限 │ ├─ 同步给坐席:完整对话 + Agent 已查到的结构化信息 + 已尝试过什么 │ 目标是坐席开口第一句不需要问「您遇到什么问题」 │ ├─ 非工作时间:明确告知等待时长或改为留工单 │ 不说「正在转接」然后没人来 │ └─ 会话状态交接:Agent 停止接管,避免双方同时回复 质量闭环 │ ├─ 评测集:真实日志抽样 + 边界用例 + 全部历史事故用例 │ 代码可判的先用代码判(有无引用 / 引用是否存在 / 数字能否回查) │ ├─ 变更即回归:提示词 / 模型版本 / 检索参数 / 分块策略,任一改动跑全量 │ ├─ 灰度发布:提示词当代码对待,小流量 ─▶ 比指标 ─▶ 全量 │ └─ 线上信号:转人工率 · 平均轮数 · 拒答率 · 单会话 token · 二次咨询率
    分步拆解
    1. 分档的依据是「答错的代价」,不是「能不能答」。这个区分是整套设计的起点。按「能不能答」划,会不断扩大自动范围直到出事;按「答错代价」划,边界是稳定的,而且和业务风险直接对应。
    2. 入口拦是最重要的一道拒答。检索相关度不够就不调模型。它同时省了钱、降了延迟、消除了编造的可能——三个收益,成本只是一个阈值判断。这道做好了,后面两道只是补漏。
    3. 拒答不能靠提示词。在系统提示里写「不知道就说不知道」,模型在相当一部分情况下会忽略,因为生成一个流畅答案对它是更「自然」的路径。必须在代码里做判定和替换。
    4. 出口校验是替换不是追加。把「以上信息可能不准确」拼在一段编造的答案后面,用户只会读前面那段。要整段替换掉。
    5. 拒答话术必须给下一步。只说「抱歉我不知道」是最差的实现,用户会重复问三遍然后骂人。三部分齐全的拒答,体验可以比一个错误答案更好。
    6. 转人工入口永远可见。不要设计「必须先试三轮」。强留只会让满意度更差,而且那三轮的成本也白花了。愿意用 Agent 的用户自己会用。
    7. 转接时把结构化信息一起给坐席,不只是聊天记录。Agent 已经查到的订单状态、物流节点、可用的优惠券,坐席拿到就不用再查一遍。这一条是坐席效率提升的主要来源,也是让坐席愿意配合推进这个项目的关键。
    8. 会话所有权要明确交接。转人工之后 Agent 必须停止接管,否则出现坐席和 Agent 同时回复的情况,用户会彻底困惑。实现上就是会话状态机加一个「已转人工」状态。
    9. 评测集里代码能判的部分优先用代码判。有无引用来源、引用的片段是否真实存在、答案里的数字能否在原文回查、是否命中禁用词、格式是否合法——这些是确定性断言,比任何模型判分都可靠且免费。
    10. 每次线上事故都往评测集里加一条。这是评测集持续增值的唯一机制。历史事故用例是整个集合里最有价值的部分,因为它们已经证明过会出问题。
    11. 提示词当代码对待:有版本、走灰度、可回滚。它看起来只是「改了几句话」,但它是全局生效的,影响面比大部分代码改动都大。
    12. 锁定模型版本号。不锁的话服务方静默更新会让效果突变,而你完全找不到原因。锁定之后配合定期跑评测集,才能把「外部变化」和「自己的改动」区分开。
    关键决策与取舍

    定位选了「分流」而不是「替代」,这个决定影响了后面所有设计。如果目标是替代人工,就必须追求「什么都能答」,那么拒答就变成了失败,团队会不断放宽自动范围直到出事。定位成分流之后,「答不了」是一个正常的、被设计过的分支,只需要答好那部分代价低的问题就有价值。代价是自动化率的天花板不高(一半多一点),但这个方案能真正上线并稳定运行,而追求替代的方案通常在出一次事故后就被叫停了。

    考核指标坚持双指标,拒绝只看自助解决率。这一条是我们和产品争论最久的。只考核自助率的后果非常明确:它会驱动团队把转人工入口藏起来、让 Agent 硬答、把「用户放弃了」计为「已解决」。我们最后定的是自助解决率、满意度、以及转人工后的二次解决时长三个一起看。第三个指标是关键——它衡量的是「转接质量」,如果上下文没带过去,这个数字会明显变长。只有把它纳入考核,转接质量才有人负责。

    置信度判定放在检索层而不是模型层。另一种思路是让模型自己输出置信度或者「是否在资料中找到依据」。我们试过,问题是模型对自己的置信度估计不可靠,它在编造时往往同样自信。而检索相关度是一个客观的、可观测的数值,用它做阈值判断更稳。代价是会误拒一部分——资料确实存在但表述差异大导致相关度低。这部分我们用查询改写来缓解,而不是放宽阈值。取舍方向是宁可多拒一点,不要硬答。

    踩过的坑一:转人工之后 Agent 还在回复。会话状态机漏了「已转人工」这个状态,坐席在回复的同时用户又发了一条消息触发了 Agent,两边同时说话。用户截图投诉。修法是加显式状态并在入口做拦截,同时把这个用例写进回归。这个坑说明会话所有权必须是状态机里的一等概念,不能靠约定。

    踩过的坑二:情绪识别过于敏感,把正常用户都转走了。加了情绪识别自动转人工,阈值设得太低,用户说一句「怎么这么慢」就被转人工,人工队列瞬间被打满。修法是把情绪识别的输出从「直接转人工」改成「提升优先级 + 调整话术」,只有明确的强烈负面才转。这条的教训是自动转人工的触发条件也需要限流——它会把压力转移到一个容量有限的资源上。

    踩过的坑三:评测集只有正常用例,跑起来一直是满分。最初的评测集是从日志里挑「典型问题」,全是正常场景,任何改动都是 100% 通过,等于没测。修法是补三类用例:知识库里没答案的(考拒答)、问法很偏的(考召回)、以及带诱导的(「你们是不是承诺无条件退款」,考会不会乱承诺)。补完之后通过率降到七成多,这才是有信息量的数字。

    没做的部分:没做坐席回复的自动学习(把人工的优质回答自动沉淀成知识)。方向是对的,但它需要人工标注哪些回答值得沉淀,而坐席没有这个动力,硬推会变成负担。这是流程和激励问题,不是技术问题,所以当时只做到了「一键把这段对话提交为知识库候选」,由知识运营同学审核。

    数字是怎么测的

    「用户重述比例」怎么统计:这是我们衡量转接质量的核心指标。做法是看转人工后用户的第一条消息和转接前的对话是否高度重叠——用文本相似度自动判定,再人工抽样校准。报「大幅下降」而不报精确百分比,因为自动判定有误差,而且这个数字受咨询类型分布影响很大。面试时我会直接说明这个指标是怎么算的以及它的局限,说清测量方法比给一个漂亮数字更能说明你真做过

    自助解决率的口径必须说清,这是最容易被追问穿的地方。分母是什么(所有进入 Agent 的会话,还是排除掉一进来就点转人工的)、分子怎么算(没有转人工就算解决?用户主动结束就算解决?)。「用户没转人工」不等于「问题解决了」——他可能是放弃了。我们的口径是「未转人工且未在 24 小时内就同一问题再次咨询」,把二次咨询的排除掉。这个口径比通用口径严,数字会低一些,但它经得住追问。

    评测集通过率:报绝对的通过用例数和总用例数,以及按类别拆开(正常 / 拒答 / 偏问法 / 诱导 / 历史事故)。历史事故那一类必须是全通过,这一类挂了就是回归,不允许发版。其他类别允许有一定失败率,因为里面有些用例本身就是难的边界情况。

    拒答率要区分「主动拒答」和「兜底拒答」。前者是入口拦(检索不到),后者是出口校验拦下的。这两个数字的意义完全不同——主动拒答率高说明知识库覆盖不够,兜底拒答率高说明模型在编造。混在一起看就丢掉了诊断价值。

    转人工率的基线要说清怎么来的。不能只报一个数字,要说明「这是在只放开查询类和政策类的前提下」。放开范围不同,这个数字完全不可比,脱离范围谈转人工率是没意义的。我们的做法是按咨询类型分别报。

    灰度阶段的对比方法:同一时间窗内小流量组和对照组比,不用「改之前 vs 改之后」。因为咨询量和咨询类型有明显的时段和周期规律,前后对比会被这些因素污染。这是标准的 A/B 做法,但在提示词变更上常被忽略,因为大家不把改提示词当成一次发布。

    面试追问
    Q:你一直在说宁可拒答,那 Agent 的价值在哪?用户还是要找人工。 A:价值在于它接住了咨询量的大头,而那部分本来也在占用人工。真实的咨询分布是长尾的——「我的快递到哪了」「运费怎么算」「怎么申请退货」这类重复问题占了一半以上,它们有确定答案、答错代价低,正是 Agent 最适合的部分。把这一半分流掉,人工就能把时间放在真正需要判断的那一半上,整体的解决速度和质量都会上升。反过来说,如果为了追求更高的自动化率去硬答那些复杂问题,省下的人工成本会被投诉处理和信任损失吃掉,而且这个损失很难量化所以容易被忽略。我会强调一点:这个方案的价值不是「替代了多少人」,是「让人工的时间用在了该用的地方」——这也是我们最后说服业务方的角度。
    Q:为什么不直接让模型自己判断「我这个答案有没有把握」,非要用检索相关度? A:试过,不可靠。模型对自己置信度的估计,在它编造的时候和它答对的时候没有明显区别——这是它的生成机制决定的,它不是在查事实所以也不知道自己没查到。让它输出「是否在资料中找到依据」稍好一些,但仍然会出现「资料里没有它却说找到了」的情况。而检索相关度是一个外部的、客观的、可观测的数值,用它做闸门更稳,而且可以调阈值、可以监控、可以复现。这里体现的是一个通用原则:做判定要用系统外部能观测到的量,不要用被判定对象的自我评价。代价是会误拒一些真实存在但表述差异大的内容,我们用查询改写去缓解,而不是靠放宽阈值——放宽阈值等于把闸门拆了
    Q:提示词也要走灰度发布,这是不是太重了?改一句话而已。 A:恰恰因为「只是改一句话」所以更危险——它的改动成本极低但影响面是全局的,这个组合最容易出事。改一行 Java 代码大家都知道要测,改一句提示词往往直接上线,而它会同时影响所有场景。我们真的踩过:为修一类问题调了提示词,两周后发现另一类问题复发了。而且提示词的影响是非线性的——加一句「回答要简洁」可能让模型把重要的免责说明也删掉了。所以我们的结论是提示词是代码的一部分,同样需要版本、评审、灰度、回滚。实操上并不重:评测集是自动跑的(几分钟),灰度是配置开关(按用户哈希分流),加起来比人工验证十条 case 还快。重的是建这套机制的一次性成本,之后每次变更的边际成本很低,这个区分要说清。
    Q:如果面试官问你这个项目最大的风险是什么,你怎么答? A:我会说最大的风险不是技术风险,是「指标驱动的边界扩张」。系统上线后会有持续的压力要求提高自动化率——业务方看到人工成本还在,会问「为什么这类问题不能也自动答」。而每一次放宽边界,单看都是合理的,累积起来就会越过那条「答错代价可接受」的线,然后出事故。技术上的防御是把边界做成显式配置并且有审批(改自动化范围要走变更流程,和改数据库权限一个级别),但根本上这是个组织问题。所以我们从一开始就把「答错代价」这个划分依据写进了设计文档并和业务方对齐——有了共识的依据,后面每次讨论扩边界才有得谈,否则就只能凭感觉拉锯。这也是我认为这类项目和纯技术项目最不一样的地方:它的长期成败取决于边界能不能守住,而不是当初做得多好。

没有匹配的内容,换个关键词试试。

项目拆解 · 智能客服 Agent 平台(后端)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据