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

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

这一页是三个 Agent 项目里最适合初级和实习生写的 它是纯前端工作、能自己搭出来、而且不需要你有生产环境的模型调用数据——本地起个服务造一批假轨迹就能把三个模块都做出来。相比另外两页,它被追问时的风险最低,因为考的是前端功底(大数据量渲染、diff、状态管理)而不是模型经验。
项目背景设定 给客服 Agent 配套的运营后台,Vue 3 + TypeScript + Pinia + Vite。使用者是三种人:排查线上问题的工程师调提示词的运营维护知识库的内容同学。三个模块正好对应他们各自最痛的一件事。
为什么选这三个模块 Agent 系统最缺的不是模型能力,是「出了问题看不见」。这三块解决的是可观测性的三个层次:单次会话为什么错了(轨迹回放)、这次改动会不会让别的场景变差(版本管理)、整体质量在往哪个方向走(评测看板)。前端上的难点是数据体积(一次会话轨迹是普通业务数据的几十倍)和差异呈现(要让人一眼看出哪一轮开始偏离)。

模块一:会话轨迹回放

  1. 会话轨迹回放(列表与详情分离加载 + 大对象按需展开 + 轮次间输入 diff + 断点定位)★★★
    简历这样写 Agent 会话轨迹回放视图(Vue 3 + TypeScript + Pinia + 虚拟滚动 + 增量拉取):一次会话轨迹含每轮完整输入、模型输出、工具入参与返回,体积是普通业务数据的几十倍,因此把索引与正文分离(列表只拉摘要,详情按轮次懒加载,单轮内的大对象再折叠按需请求);时间线的目标是定位「哪一轮开始偏离」,所以对相邻轮次的输入做结构化 diff(只高亮新增与被裁剪的片段)而不是各轮完整平铺;对工具失败、检索零结果、校验拦截等关键事件打断点标记并支持一键跳转。改造后单条轨迹首屏渲染的数据量降到原来的一个小比例,工程师定位一次问题的操作步数明显减少。
    展开完整拆解
    为什么要这么设计

    第一版就是「把后端返回的轨迹 JSON 全部渲染出来」,一个可折叠的树形结构。功能上没错,但实际用起来三个问题让它几乎没人用。

    一是打不开。一次十几轮的会话,每轮的输入都包含完整的系统提示、对话历史、检索到的文档全文,整个 JSON 有几 MB。一次性拉取加渲染,页面卡死好几秒,偶尔直接崩。而排查问题的场景往往是线上告警之后,工程师最需要它快的时候它最慢。

    二是看不出问题在哪。十几轮内容平铺出来,每轮的输入有 90% 是和上一轮重复的(系统提示、大部分历史都一样)。工程师要在大量重复内容里找那 10% 的变化,实际操作是把两轮的 JSON 复制出来丢进 diff 工具——那说明这个界面没有提供它该提供的价值。

    三是找不到关键事件。「哪一轮的工具报错了」「哪一轮检索是空的」这些是排查的入口,但它们埋在树形结构的深处,要一层层展开才能看到。

    所以第二版重新定位:这个界面的目标不是「展示轨迹」,是「定位到出错的那一轮」。三个改动对应三个问题:索引与正文分离、按轮次和大对象两级懒加载(解决打不开)、相邻轮次输入做结构化 diff(解决看不出)、关键事件打断点标记并可跳转(解决找不到)。

    最值得说的判断是第二条:轨迹回放的核心视图不是「每轮的完整内容」,而是「每轮相对上一轮变了什么」。因为 Agent 出错的原因几乎总是「这一轮新进来的东西」——新的检索结果、新的工具返回、被裁剪掉的历史。把不变的部分默认收起、只呈现变化,信息密度提升了一个量级,而这个改动纯粹是前端的视图设计,不需要后端配合。

    整体链路
    数据分层(前端驱动的接口拆分) │ ├─ 层一 会话列表:只要摘要字段 │ 会话ID / 用户 / 时间 / 轮数 / token / 是否异常 / 结束方式 │ 支持按异常类型筛选,这是排查的主要入口 │ ├─ 层二 单会话骨架:每轮的元信息,不含正文 │ 轮次 / 耗时 / token / 调了哪些工具 / 是否有失败事件 │ 体积小,一次拉完,用来渲染时间线和断点标记 │ └─ 层三 单轮正文:按需拉取 用户消息 / 模型输出 / 工具入参与返回 / 完整输入 其中「完整输入」和「工具返回原文」再折一层,点开才请求 时间线(左侧,常驻) │ ├─ 每轮一个节点,节点上叠加事件标记 │ 工具失败 · 检索零结果 · 校验拦截 · 参数重试 · 重复调用 │ ├─ 异常轮次高亮,支持「只看异常轮」过滤 │ └─ 点节点 ─▶ 右侧加载该轮详情(懒加载 + 已加载缓存) 详情区(右侧)默认三段,按信息密度排序 │ ├─ 1 本轮变化(默认展开,这是核心视图) │ 与上一轮输入做结构化 diff: │ 新增的检索片段 / 新增的工具返回 / 被裁剪掉的历史轮次 │ 不变的系统提示与历史默认折叠,标注「与上轮相同」 │ ├─ 2 模型输出与工具调用(默认展开) │ 输出原文 · 工具名与入参 · 返回摘要 · 耗时 │ └─ 3 完整输入(默认折叠,点开才请求) 用于确认「模型当时到底看到了什么」,是最终依据 版本信息(顶部固定条) └─ 提示词版本 · 模型版本 · 检索配置版本 · 知识库快照 没有这一条,回放出来的东西无法和当时对应
    分步拆解
    1. 先推动接口按三层拆开,这是前端主动提的需求。后端原本只有一个「取完整轨迹」的接口,前端拿到几 MB 只用其中一小部分。这类优化必须从接口层面解决,前端再怎么优化渲染都救不了传输。骨架接口体积很小,是整个方案的基础。
    2. 时间线用骨架数据渲染,不依赖正文。骨架里有每轮的元信息和事件标记,足够画出完整的时间线和断点。用户打开页面立刻能看到「一共几轮、哪几轮有问题」,这就是最关键的信息,正文可以慢慢加载。
    3. 正文按轮次懒加载并缓存。点某一轮才请求该轮正文,加载过的存在 Pinia 里,来回切换不重复请求。缓存要有上限——一个几十轮的会话把所有正文都缓存下来内存会很大,我们保留最近查看的若干轮,超出的丢弃。
    4. 单轮内的大对象再折一层。「完整输入」和「工具返回原文」这两块是体积主要来源,默认不请求,点开才拉。两级懒加载的划分依据是「使用频率」:模型输出和工具入参是每次都要看的,完整输入是确认最终依据时才看的。
    5. diff 是这个模块的核心,要做成结构化的而不是文本的。不要把两轮的 JSON 字符串丢给文本 diff 库——那样会把格式差异、字段顺序都算成变化,噪音极大。要按语义单元 diff:检索片段按片段 ID 比对(新增了哪几个)、历史轮次按轮次 ID 比对(少了哪几轮)、系统提示按整体哈希比对(变了还是没变)。
    6. 「被裁剪掉的历史」是最有价值的一类 diff。Agent 忘掉早期约束的故障,在这个视图里表现为「第 N 轮开始,第 1 轮的内容不在输入里了」。把裁剪事件显式呈现出来,这类问题从难查变成一眼可见。
    7. 不变的部分要标注而不是隐藏。系统提示折叠起来的同时要写明「与上轮相同(哈希 abc123)」。「相同」本身是有信息量的结论,直接不显示会让人怀疑是不是漏加载了。
    8. 关键事件的标记种类要收敛,不要什么都标。我们最后只留了五类(工具失败、检索零结果、校验拦截、参数重试、重复调用),因为标记太多就等于没有标记。判据是「这个事件出现时,是否值得工程师专门看一眼」。
    9. 版本信息必须固定在顶部常驻。提示词版本、模型版本、知识库快照。回放的前提是知道当时的配置是什么,否则你看到的行为和现在的配置对不上,会得出错误结论。这一条是后端提供数据、前端负责让它不被忽略。
    10. 要支持「用当前配置重跑这一轮」。排查的常见动作是「拿当时的输入,用现在的提示词再跑一次,看是否已经修好」。这个按钮把回放从「只读的历史」变成了「可验证的假设」,是工程师最常用的功能之一。要注意重跑必须走沙箱、不能触发有副作用的工具。
    关键决策与取舍

    diff 选了结构化而不是文本 diff,代价是要为每种内容类型写比对逻辑。文本 diff 库现成、几行就能接上,但它对 JSON 的效果很差——字段顺序变化、格式化差异、长文本内部的换行都会被算成变化,真正的语义变化被噪音淹掉。结构化 diff 要为检索片段、历史轮次、工具返回各写一套比对规则,工作量明显更大。但这个模块的全部价值就在于「让变化一眼可见」,diff 做不准整个模块就没意义了。取舍的判据是「这部分是不是这个功能的核心价值」——是核心就不要用现成的通用方案凑。

    两级懒加载 vs 一次拉完再前端处理。如果一次拉完,前端可以做得更流畅(切换轮次零延迟)。但轨迹体积决定了这不可行——几 MB 的传输和解析在告警时段是不可接受的。代价是切换轮次有网络延迟,我们用两个手段缓解:预取相邻轮次(看第 5 轮时后台预拉第 4 和第 6 轮,因为排查时前后翻是最常见的操作)、以及已加载的缓存不释放(在上限内)。这个组合下实际体验接近本地切换。

    「重跑这一轮」放在前端做还是后端做,我们选了后端提供沙箱接口、前端只负责发起和展示对比。前端不碰模型调用,避免密钥暴露和成本失控。关键约束是沙箱必须禁用有副作用的工具——重跑一个包含「发优惠券」的轮次,不能真的再发一张。这一点是前端提出来的需求,因为最初的实现真的重复发了券。

    踩过的坑一:虚拟滚动用在了不该用的地方。一开始把「完整输入」这种长文本也做了虚拟滚动,以为能提升性能。结果是浏览器的原生搜索(Ctrl+F)失效了——虚拟滚动只渲染可见区域,搜不到未渲染的内容。而工程师查轨迹最常用的操作恰恰是全文搜索一个订单号。修法是长文本改为「折叠 + 完整渲染」:默认折叠不占性能,展开后完整渲染保证可搜索。这个判断和报表页选 DOM 不选 Canvas 是同一类考虑——要先问清用户会对这块内容做什么操作。

    踩过的坑二:时间线的事件标记设计了十几种,没人看得懂。不同颜色不同图标,用户需要对照图例才能理解。修法是砍到五类,并且用文字标签而不是纯图标。删掉的那些不是不重要,而是「知道了也不会改变下一步动作」,那就不该占据视觉资源。

    没做的部分:没做多会话的对比视图(同时打开两个会话左右对照)。需求是真实的(「为什么这个用户对那个用户错」),但界面复杂度上升很多,而且实际排查中更常用的是「同一会话的轮次间对比」,这个已经做了。把它列为已知需求但明确没做,比硬做一个半成品好。

    数字是怎么测的

    首屏数据量:用浏览器网络面板量首屏请求的总传输字节,对比改造前后同一条轨迹。要说清测的是哪条轨迹——轨迹体积差异很大,用「一条十几轮的典型会话」这种描述,并且说明轮数,否则数字不可比。我们报的是「降到原来的一个小比例」而不是精确倍数,因为它取决于会话长度。

    渲染耗时:用 Performance 面板量从请求返回到时间线可交互的时长,取多次的中位数。关键是要量「可交互」而不是「渲染完成」——用户能点第一个节点就算可用了,正文还在加载不影响。这个区分让指标和真实体验对齐。

    定位效率用操作步数而不是时间。做法是给几个真实的历史问题,让工程师用新旧两版界面各定位一遍,记录点击和展开的次数。用步数不用时间的理由是:时间受熟练度和问题难度影响太大,样本又少(我们只找了三四个同事试),步数是更客观的代理指标。报法是「操作步数明显减少」加上具体的对比场景描述,不编精确百分比。

    内存占用:连续查看二十轮之后看 Performance 里的堆快照,确认缓存上限生效、没有持续增长。这个必须测,因为轨迹正文很大,缓存不设上限的话查几个会话就能吃掉几百 MB。

    diff 正确性用构造用例。造几组轨迹(新增检索片段、裁剪历史、系统提示变更、工具返回变化),断言 diff 结果准确标出了变化项且没有误报。误报比漏报更该重点测——一个到处标着「变化」的 diff 视图会让人立刻不信任它。

    面试追问
    Q:接口按三层拆开是后端配合的,如果后端不配合,纯前端能做到什么程度? A:能做但上限明显低。纯前端的做法是拿到完整轨迹后在前端做分层——用 Web Worker 解析大 JSON 避免阻塞主线程、用虚拟化只渲染可见轮次、diff 和折叠逻辑完全在前端做。这样能解决「渲染卡」但解决不了「传输慢」,几 MB 的下载和解析时间还在,而且移动端和弱网下更明显。所以我会先做前端能做的部分把体验拉到可用,同时用实测数据去推动接口拆分——带着「首屏传输 4MB、其中实际用到 200KB」这个数字去谈,比说「接口太重」有效得多。这也是我认为前端在这类项目里的价值所在:不只是消费接口,而是基于对使用场景的理解去定义接口该长什么样。最终我们的骨架接口就是前端设计的字段列表。
    Q:你说不用文本 diff 而用结构化 diff,那如果内容格式变了,比如后端新加了一个字段,你的 diff 逻辑会不会挂? A:会退化但不该挂,这是设计时要考虑的。我们的做法是比对逻辑按已知的语义单元处理,遇到不认识的字段走兜底——兜底是「整体哈希比对」,只判断变了还是没变,不细分变在哪。所以新加字段的表现是「能看出这里变了,但看不出具体差异」,功能降级而不是报错。同时我们在开发环境加了一个校验:遇到未知字段就在控制台警告,提示需要补比对规则。这个模式的通用价值在于:任何依赖上游数据结构的前端逻辑,都要区分「认识的部分精细处理、不认识的部分安全降级」,而不是假设结构永远不变。另外一个配套做法是把轨迹的数据结构定义成前后端共享的 TypeScript 类型,后端改结构时类型检查会先报错——这样问题在编译期就暴露,不用等到运行时。
    Q:虚拟滚动导致 Ctrl+F 失效这个坑,为什么不自己实现一个搜索功能就好了? A:可以,我们也确实为时间线做了自己的搜索。但对「完整输入」这块长文本,我判断自己实现搜索不如放弃虚拟滚动,理由有三个。一是用户预期——工程师看到一大段文本,第一反应就是 Ctrl+F,自己实现的搜索框他可能压根没注意到,体验上是在跟习惯对抗。二是功能差距:浏览器原生搜索支持增量高亮、上下跳转、大小写切换,自己实现要做到同样程度成本不低,而且做不到「跨整个页面搜索」。三是实际收益不大:这块内容是折叠的,默认不渲染就没有性能问题,展开的时候用户就是要仔细看它,一次完整渲染的代价可以接受。所以判断依据是「这块内容的交互模式是浏览还是精读」——浏览用虚拟滚动,精读就完整渲染保留原生能力。时间线是浏览(几十个节点快速扫),长文本是精读,两者用不同策略。

模块二:提示词与工具版本管理

  1. 提示词与工具版本管理(发布前影响面预演 + 语义单元 diff + 灰度配置 + 一键回滚)★★★
    简历这样写 提示词与工具配置的发布台(Vue 3 + Monaco Editor + 差异视图 + 灰度配置 + 乐观锁):把提示词当代码对待——版本化、可 diff、可灰度、可回滚;发布界面的核心不是文本差异而是影响面预演:改动后自动在评测集上试跑,直接列出「从通过变失败」和「从失败变通过」的用例,前者非空即阻断发布;diff 按语义单元(段落 / 变量占位符 / 工具描述)呈现而非纯行级,并对占位符做结构校验(漏写或拼错变量名在提交前拦下);多人协同用乐观锁加变更提示,避免后保存者静默覆盖。上线后「改提示词导致其他场景回归」的问题由发布前拦截,历史事故用例在预演中保持零变差。
    展开完整拆解
    为什么要这么设计

    第一版是个很朴素的表单:一个大文本框,改完点保存,立刻全量生效。这个界面在两周内引发了三次线上问题。

    一是改一处坏另一处。运营为了让回答更简洁,在提示词里加了一句「回答控制在三句话以内」。上线后简洁了,但模型把必要的免责说明也删掉了,涉及退款政策的回答变得不完整。而这个副作用在改的时候完全预料不到,是客户投诉才发现的。

    二是变量占位符写错,静默失效。提示词里有 {userName} 这类占位符,有人手打成了 {userNmae}渲染时占位符没被替换,原样进了模型的输入,模型看到一个奇怪的花括号内容,行为变得不稳定。没有任何报错,排查了半天。

    三是两个人同时改,后保存的覆盖了前面的。运营在调话术、工程师在改工具描述,两人打开的是同一份,谁后点保存谁的版本生效,另一个人的改动无声消失。

    更根本的问题是:大家不把「改提示词」当成一次发布。它只是「改了几句话」,所以不评审、不测试、不灰度、直接全量。但它的影响面比大部分代码改动都大——它是全局生效的,同时影响所有场景。

    所以第二版的核心思路是把提示词当代码对待,并且把「测试」这一步做进发布界面本身。三个改动:发布前自动在评测集上预演并展示变差用例(解决改一处坏另一处)、提交前做占位符结构校验(解决静默失效)、乐观锁加变更提示(解决覆盖)。

    最值得说的判断是第一条:这个界面要给的不是文本差异,是「这次改动会让哪些用例从通过变失败」。文本 diff 只能告诉你改了什么字,而提示词的改动和效果之间没有直观的对应关系——加一句话可能影响一个完全没想到的场景。所以真正有决策价值的信息是评测结果的差异,而不是文本的差异。把这个信息放在发布按钮旁边,人才会在按之前看一眼。

    整体链路
    编辑区(左右分栏) │ ├─ 左:编辑器(Monaco) │ 占位符语法高亮 + 可用变量的自动补全 │ 从上下文声明里取可用变量,不靠人记 │ └─ 右:当前生效版本(只读) 始终可见,避免改的时候忘了原来是什么 提交前校验(本地,即时反馈) │ ├─ 占位符结构校验 │ 拼错的变量名 / 未闭合的花括号 / 声明了但未使用 │ 拼错是最高频也最静默的错误,必须在提交前拦 │ ├─ 长度与预算提示 │ 系统提示变长会挤压历史预算,且影响每次调用成本 │ 实时显示 token 估算与占预算比例 │ └─ 固定前缀是否被破坏 变化内容必须后置,否则提示词缓存全部失效 这一条纯代码可判:检查变化位置是否在固定段之后 diff 视图(按语义单元,不是纯行级) │ ├─ 段落级:哪一段被改动 / 新增 / 删除 ├─ 占位符级:变量的增删改单独列出 ├─ 工具描述级:哪个工具的描述变了 └─ 固定前缀段变更单独标红(影响缓存命中) 影响面预演(这个界面的核心) │ ├─ 点「预演」─▶ 用新版本在评测集上跑一遍(沙箱,禁副作用工具) │ ├─ 结果按三分呈现,不给单一通过率 │ 变差:通过 ─▶ 失败 这一栏非空即阻断发布 │ 变好:失败 ─▶ 通过 │ 不变:保持原状 │ ├─ 变差用例可直接点进去看新旧输出对比 │ 要看到「为什么变差」,不只是知道它变差了 │ └─ 历史事故用例单独成组,这一组任一变差都硬阻断 发布(灰度) │ ├─ 按用户哈希分流,比例可调(先小流量) │ ├─ 灰度期间对照核心指标:转人工率 / 拒答率 / 平均轮数 / 满意度 │ ├─ 一键全量 / 一键回滚(回滚是切版本指针,不是反向编辑) │ └─ 发布记录:谁 / 何时 / 改了什么 / 预演结果 / 灰度数据 协同 │ ├─ 乐观锁:提交时带基线版本号,不匹配即拒绝并展示对方改动 │ └─ 编辑中的软提示:「张三 3 分钟前也在编辑这一份」 非硬锁,只提示,避免锁住之后忘记释放
    分步拆解
    1. 可用变量从上下文声明里取,做成自动补全。不要让人凭记忆手打占位符。这一条从源头上消灭了拼错变量名这类错误的大部分,比事后校验更有效。
    2. 占位符校验必须在提交前做,而且要拦住三种情况:拼错的变量名(不在声明列表里)、未闭合的花括号、以及声明了但没被使用的变量。第三种是提示——可能是改的时候删掉了一句话,而那句话是唯一用到某个变量的地方。
    3. 实时显示 token 估算和占预算比例。系统提示变长有两个后果:每次调用的成本上升、以及挤压对话历史的预算。把这个代价在编辑时就显示出来,运营才会意识到「多写两句话」不是免费的。
    4. 校验固定前缀有没有被破坏,这一条很少有人做但很值钱。提示词缓存依赖前缀固定,如果有人在开头加了动态内容(时间戳、用户名),缓存全部失效、输入成本翻几倍,而功能上完全正常没有任何报错。这个校验就是检查变化位置是否落在声明的固定段之后,纯代码可判。
    5. diff 按语义单元而不是按行。提示词是自然语言,一段话重新换行会让行级 diff 显示成整段重写。按段落、占位符、工具描述三个维度分别 diff,才能看清实际改了什么。
    6. 影响面预演是这个界面的核心功能,要放在发布路径上。不能做成一个「可选的检查」放在角落——必须是发布流程的一步,预演没跑过不允许发布。
    7. 预演结果按「变差 / 变好 / 不变」三分,不要只给一个通过率。通过率从 82% 到 83% 看起来是提升,但可能是修好了三个、弄坏了两个,而弄坏的那两个恰好是核心场景。三分呈现让这件事无法被平均值掩盖,这是整个设计里最关键的一个决定。
    8. 变差的用例要能一键看新旧输出对比。只告诉人「这条变差了」不够,他需要看到新版本输出了什么、哪里不对,才能判断是真的变差还是评测标准太严。
    9. 历史事故用例单独成组并硬阻断。这一组是已经出过线上问题的场景,任何一条变差都不允许发布,没有例外。其他组允许人工判断后强制发布(要填理由),事故组不允许。
    10. 灰度按用户哈希分流,不按时间。按时间分(比如先在夜间发)会被时段的流量结构差异污染,无法对比。按用户哈希分流可以同时段对照。
    11. 回滚是切版本指针,不是反向编辑。让人手动把内容改回去,很容易改漏或改错。版本化之后回滚就是把生效指针指向上一个版本,一次操作、无出错空间。
    12. 乐观锁用软提示配合,不用硬锁。硬锁的问题是有人打开编辑器就去开会了,锁一直不释放。乐观锁(提交时校验基线版本)加上「有人也在编辑」的软提示,既避免静默覆盖又不会卡住流程。冲突时要展示对方改了什么,而不是只说「版本冲突请刷新」。
    关键决策与取舍

    预演做成阻断式还是提示式,我们选了「分组差别对待」。全部阻断会让流程太重——评测集里有些用例本身是难的边界情况,偶尔失败是正常的,如果任何变差都阻断,运营会永远发不出去,最后一定会想办法绕过整个机制。全部只提示则等于没有约束。所以按用例的性质分:历史事故组硬阻断(不允许强制发布),其他组允许强制发布但必须填写理由并记入发布记录。这个设计的关键是「让绕过变得可见而不是变得不可能」——完全堵死会催生绕过机制的行为,可见的绕过反而能被管理。

    diff 选按语义单元做,代价是要维护段落切分规则。提示词的段落边界不像代码那么明确,我们的做法是要求提示词用固定的分段标记组织(每个功能段有标题),这样切分就是确定的。这相当于给提示词加了一个轻量的结构约束。代价是运营写的时候要遵守格式,收益是 diff 准确、影响面清楚、而且强迫大家把提示词写得有结构而不是一大段流水账。这个约束后来被证明比 diff 本身更有价值。

    编辑器用 Monaco 而不是普通 textarea。需要的是语法高亮(占位符)、自动补全(可用变量)、以及和 diff 视图的一致体验。代价是包体积明显增加,所以做了路由级懒加载,只有进这个页面才加载。这个取舍在内部工具上是划算的——使用者是每天要改十几次的运营,编辑体验的收益远大于首次加载慢半秒。如果是给终端用户的低频功能,我不会引入它。

    踩过的坑一:预演跑得太慢,没人愿意等。评测集有几百条用例,串行跑一遍要几分钟,运营点了预演就去干别的了,回来忘了看结果直接点发布。修法是两条:并发跑(受限于模型服务的限流,我们设了并发上限)、以及分层跑——先跑历史事故组和核心组(几十条,几十秒出结果),这批过了就允许发布,全量集在后台继续跑完再补充报告。关键的判断是:阻断性检查必须快到人愿意等,否则它会被绕过。

    踩过的坑二:diff 视图默认展开全部,改一个字看到满屏绿色。因为提示词很长,即使只改了一句,整个文档都渲染在 diff 视图里,用户要滚很久才找到改动处。修法是默认只显示有变化的段落,未变化的段落折叠成一行「N 段未变化」。这和轨迹回放那边「只呈现变化」是同一个原则,两个模块都是这个思路。

    踩过的坑三:乐观锁冲突的提示太粗糙。最初只显示「版本冲突,请刷新后重试」,用户刷新之后自己的改动全丢了,得重新写一遍。修法是冲突时把三方内容都展示出来:你的版本、对方的版本、共同的基线,让用户自己选择合并。「冲突提示必须保住用户已经付出的劳动」,这是这类协同场景的基本要求。

    没做的部分:没做提示词的 A/B 效果自动判定(自动决定哪个版本更好然后全量)。技术上可以做,但「哪个版本更好」在很多情况下不是单一指标能判定的——简洁度上去了但完整性下来了,谁更好取决于业务当下的优先级。这个判断应该由人做,系统的职责是把数据摆清楚。我们只做到了展示对照数据和一键切换。

    数字是怎么测的

    「发布前拦截」的效果怎么证明:统计预演阶段被判定为变差、因此没有发布出去的次数,以及其中被人工确认「确实是问题」的比例。后一个数字很重要——如果拦下来的大部分是误判(评测标准太严),那这个机制只是在添麻烦。我们的做法是每次拦截都要求填「是真问题还是误判」,用这个数据反过来校准评测集。

    预演耗时按分层报。核心组(几十条)的耗时是阻断性的,必须报;全量集的耗时是参考。要说明并发数和是否受模型服务限流,否则这个数字不可比——同样的用例数,并发 5 和并发 20 差好几倍。

    占位符校验的价值用「拦下的次数」衡量。上线后统计提交时被校验拦住的次数和类型分布(拼错变量名 / 未闭合 / 声明未使用)。这是个直接的、无争议的计数,比任何间接指标都清楚。而且它能反过来说明这类错误有多高频——我们的数据里拼错变量名占了大头,印证了自动补全的必要性。

    回滚耗时:从点回滚到新配置生效的端到端时长。这个指标的意义是「出事之后多久能止损」,所以要量的是真实的生效时间(含配置下发和缓存刷新),不是接口响应时间。我们的做法是回滚后立刻发一个探测请求确认生效版本。

    编辑器的加载耗时:因为引入了 Monaco,要单独量这个页面的首屏时间并和其他页面对比。报的时候要说明做了路由级懒加载,否则面试官会问「引入这么大的编辑器不影响性能吗」——主动说清缓解手段。

    面试追问
    Q:既然提示词要当代码对待,为什么不干脆放到代码仓库里走正常的 PR 流程? A:这个方案我们认真考虑过,最后没选,理由是使用者不是工程师。改提示词的主要是运营和客服业务专家,让他们用 Git 和 PR 的成本很高,实际结果会是「他们把改动需求提给工程师,工程师代改」——这会让迭代速度慢一个数量级,而提示词调优恰恰需要高频快速迭代。而且业务专家才是真正知道话术该怎么写的人,中间加一层传话必然失真。所以我们的选择是「把 PR 流程的实质搬进一个业务人员能用的界面」:版本化对应提交历史、diff 对应代码审查、预演对应 CI、灰度对应分阶段发布、回滚对应 revert。形式上不是 Git,实质上是同一套约束。另外补一点:提示词的模板结构和变量声明确实放在代码仓库里,由工程师维护并走 PR,业务人员只能改文案不能改结构——把「谁能改什么」分开,两套流程各管一块。
    Q:预演允许「填理由强制发布」,那不就等于可以随便绕过? A:能绕过,这是有意的设计。我的判断是「完全堵死会催生更坏的绕过方式」——如果强制不允许发布,实际会发生的是有人去改评测集把失败的用例删掉,或者直接找工程师从后台改配置,那时候绕过就完全不可见了。允许强制发布但留下痕迹(谁发的、当时哪些用例变差、填了什么理由),至少这件事是可审计的、可以事后复盘的、也可以做成月度的数据去讨论。而且实践中「要填理由」本身就有很强的约束力——需要写下「我知道这三条用例会变差,但我认为可接受因为……」的时候,人会认真想一遍。唯一的例外是历史事故组,那一组硬阻断没有强制发布的入口,因为它对应的是已经造成过真实损失的场景,不接受判断。这个「大部分可绕但关键项不可绕」的分层,我认为是内部工具设计里比较实用的一个模式。
    Q:你说 diff 按语义单元要求提示词有固定分段格式,如果运营不遵守怎么办? A:靠工具约束而不是靠自觉。编辑器里分段标题是结构化的输入项而不是自由文本——新建一段要点「添加段落」并填标题,正文写在段落内部。这样格式是被界面强制的,不遵守在物理上做不到。这比写规范然后指望人遵守要可靠得多。代价是灵活性下降:想写一段不属于任何分类的话,得先建一个段落。我们的处理是提供一个「其他」段落作为兜底。另外这个约束带来了一个没预料到的收益:因为提示词被迫结构化了,我们后来能做「按段落灰度」——只对某一个功能段做 A/B,而不是整份提示词一起换。这是「加约束反而打开了新能力」的例子,如果当初是一大段自由文本,按段灰度压根无从下手。这个追问值得往这个方向答,因为它说明结构化的价值不止于 diff。

模块三:评测集与回归看板

  1. 评测集与回归看板(线上日志一键转用例 + 双人标注比对 + 断言与判分分离 + 趋势归因)★★★
    简历这样写 Agent 评测集管理与回归看板(Vue 3 + Pinia + 虚拟表格 + 批量任务 + 图表):评测集的价值取决于失败与边界用例的覆盖,所以把入口做在轨迹页「一键转为用例」——线上出问题的会话可直接带输入与上下文快照落成用例,让评测集随事故持续增长;标注界面支持双人独立标注与分歧比对(先各自标、再只看分歧对齐标准),并把代码断言(有无引用 / 引用是否存在 / 数字能否回查 / 格式是否合法)与模型判分分栏展示,因为两者可信度不同不该混算一个总分;看板按变好 / 变差 / 不变归因而非只给通过率,变差用例可一键加入阻断清单。上线后评测集从人工整理转为事故驱动增长,回归中变差用例可在发布前定位到具体改动。
    展开完整拆解
    为什么要这么设计

    第一版的评测集是一个 CSV 文件,工程师手动维护,跑评测是本地跑个脚本看输出。三个问题让它形同虚设。

    一是评测集不增长。维护它是额外工作,没人有动力做。线上出了问题,大家忙着修,修完就过去了,那个宝贵的失败案例没有被留下来。三个月后同类问题复发,才发现从来没有把它加进评测。

    二是里面全是正常用例。最初的用例是从日志里挑「典型问题」,全是正常场景,任何改动跑起来都接近满分,等于没测。真正有价值的是那些会失败的、边界的、有诱导性的用例,但这些不会自然出现在「典型问题」里。

    三是标注不一致。两个人标同一条用例,对「这个回答算不算通过」的判断不同——一个觉得答案不够完整算失败,一个觉得核心信息有了算通过。标注标准不统一,评测结果就没有可比性,跑出来的通过率是个噪音数字。

    还有个更隐蔽的问题:把代码断言和模型判分混在一起算一个总分。「有没有引用来源」是确定的(代码判,一定准),「回答是否恰当」是模型判的(有偏差,会偏好长答案)。混成一个总分之后,这个分数既不精确也不可解释——分数降了不知道是真的变差还是判分模型抽风。

    所以第三版重做了四件事:把创建用例的入口放到轨迹页(在最有动力的时刻,也就是刚排查完问题的时候,一键就能留下来)、用例分组并显式区分失败与边界类双人独立标注加分歧比对断言与判分分栏展示不合并总分

    最值得说的判断是第一条:评测集能不能持续增长,取决于「添加用例」这个动作发生在什么时刻。做成一个独立的管理页面,就永远需要额外的自觉;做成轨迹回放页上的一个按钮,它就发生在工程师刚刚查完问题、上下文最清楚、最想让这个问题别再犯的那一刻。这个设计上的位置选择,比界面本身做得多好都重要。

    整体链路
    用例来源(入口设计决定评测集能否增长) │ ├─ 主入口:轨迹页「一键转为用例」 │ 带走:用户输入 + 上下文快照 + 当时的召回结果 + 版本信息 │ 发生在刚查完问题的那一刻,此时最有动力也最清楚 │ ├─ 次入口:从线上日志按条件批量抽样导入 │ 按异常类型 / 拒答 / 转人工 / 高轮数筛选后批量转用例 │ └─ 手工新建:用于构造边界与诱导类用例 这类不会自然出现在日志里,必须人为设计 用例分组(分组决定阻断策略) │ ├─ 历史事故组 硬阻断,任一变差不许发布 ├─ 核心场景组 阻断,可填理由强制 ├─ 拒答组 知识库无答案,考的是会不会硬答 ├─ 偏问法组 口语与术语差距大,考召回 ├─ 诱导组 考会不会乱承诺 └─ 长会话组 考早期约束是否被裁剪丢失 标注(解决一致性) │ ├─ 双人独立标注:各自标完前互不可见 │ ├─ 分歧视图:只列出两人结论不同的用例 │ 对齐的不用看,注意力全放在分歧上 │ ├─ 分歧讨论后沉淀为标注规范条目 │ 规范是长出来的,不是一开始写全的 │ └─ 标注一致率作为评测集健康度指标持续跟踪 评分(两类分栏,不合并总分) │ ├─ A 代码断言(确定性,权重高,可解释) │ 有无引用 · 引用是否真实存在 · 数字能否回原文查到 │ 是否命中禁用词 · 输出格式是否合法 · 是否越权 │ └─ B 模型判分(有偏差,仅作参考) 判分标准必须具体:是否引用了文档X / 是否包含退款时效字段 不问「好不好」,因为它偏好长答案与自身风格 回归看板 │ ├─ 三分归因,不给单一通过率 │ 变差 ─▶ 重点,可一键加入阻断清单并跳转新旧输出对比 │ 变好 ─▶ 确认改动是否达到预期 │ 不变 ─▶ 折叠 │ ├─ 趋势图按组分线,不看总体一条线 │ 总体线会把某一组的退化平均掉 │ └─ 每个数据点关联当时的版本信息 指标跳变时能立刻对上是哪次改动或模型版本变更
    分步拆解
    1. 把创建用例的入口放在轨迹页,这是整个模块成败的关键。不是做一个「新建用例」的表单等人来填,而是在排查现场提供一键操作。入口的位置决定了行为是否会发生,这条比界面细节重要得多。
    2. 一键转用例要带走上下文快照,不只是用户的问题。包括当时的召回结果、提示词版本、模型版本。因为很多问题的复现依赖当时的检索结果,只存一个问题文本,换个时间跑可能压根复现不出来。
    3. 用例必须分组,而且分组直接对应阻断策略。历史事故组硬阻断、核心组可强制、其他组只提示。不分组就只能一刀切,而一刀切必然是错的——要么太严发不出去,要么太松等于没有。
    4. 要显式设计「不会自然出现在日志里」的组。诱导类(「你们是不是承诺无条件退款」)、拒答类(问知识库里没有的)、长会话类。这些必须人为构造。只从日志里抽样的评测集会系统性地缺失这几类,而它们恰恰对应最严重的风险。
    5. 双人标注要「先各自标完再看分歧」。如果能互相看到,第二个人会被第一个人锚定,一致率虚高但标准并没有真正对齐。界面上要强制这个顺序。
    6. 分歧视图只显示不一致的用例。一致的不用看。这个视图的价值是把注意力压缩到真正需要讨论的地方,通常只占一小部分。
    7. 标注规范是从分歧里长出来的。每次讨论完一个分歧,把结论沉淀成一条规范。不要试图一开始就写全规范——写不全,而且写出来的多半是想象中的情况。
    8. 标注一致率本身要作为指标跟踪。它衡量的是评测集的可信度:一致率低说明标准不清,此时通过率这个数字就不可信。先修一致率,再看通过率。
    9. 代码断言和模型判分必须分栏,不合并总分。两者可信度差一个量级。合并之后分数变化无法归因——不知道是真的变差了还是判分模型偏了。分开之后,断言类指标可以直接用于阻断,判分类只作参考。
    10. 模型判分的标准要写成可核对的清单。不问「这个答案好不好」,问「是否引用了文档 X」「是否包含了退款时效」。把主观判断拆成具体条目,判分才稳定,而且界面上要把这些条目逐项展示而不是只给一个分数。
    11. 看板三分归因,变差是重点区。变差的用例要能一键加入阻断清单(下次发布这条必须过),并直接跳到新旧输出对比。「发现问题」和「防止它再发生」之间要只有一次点击。
    12. 趋势图按组分线,不画总体一条线。总体线会把某一组的退化平均掉——拒答组从 90% 掉到 60%,因为其他组样本多,总体线只降两个点,看不出来。分组分线是这个图有用的前提。
    13. 每个数据点要关联版本信息。指标突然跳变时,鼠标悬停就能看到「这天提示词从 v12 改到 v13」或者「模型版本变了」。没有这个关联,趋势图只能看出「变差了」,看不出「为什么」。
    关键决策与取舍

    双人标注的成本很高,我们只对部分用例做。全部用例双标,标注工作量直接翻倍,实际推不动。取舍是:新建用例和存在分歧历史的用例双标,其余单标。另外每个季度抽一批已有用例重新双标,用来检查标准有没有漂移。这个方案承认了「一致性保障是有成本的、只能用在最需要的地方」,比声称全量双标要真实——面试时如果说「我们所有用例都双人标注」,一问标注工时就穿了。

    模型判分留下来了,没有因为它有偏差就砍掉。它的价值在于能覆盖代码断言判不了的维度(表述是否清楚、是否答到了点上)。处理偏差的方式不是放弃它,而是限制它的用途:只作参考不用于阻断、判分标准拆成具体条目、以及固定判分模型的版本(判分模型自己升级会让历史分数不可比,这个坑很容易忽略)。

    看板选了「三分归因」而不是行业常见的「通过率趋势」。通过率是个更简洁的指标,管理层也更喜欢看。但它的问题是掩盖了此消彼长——修好三个弄坏两个,通过率是涨的。我们的妥协是顶部仍然给通过率(对上汇报用),但默认展开的是三分明细(干活用)。两个受众要的东西不同,界面上要同时满足,而不是二选一。

    踩过的坑一:一键转用例做得太顺,评测集迅速膨胀到跑不动。入口做好之后大家很愿意用,几个月加了上千条,全量跑一遍要很久,而且里面有大量重复的同类用例。修法是加「相似用例检测」——新增时提示「已有 3 条相似用例」,让人判断是否真的需要加;以及给用例加权重和采样策略,日常回归跑核心子集,全量集定期跑。这个坑说明「让某个行为变容易」之后必须同时考虑它的量的治理。

    踩过的坑二:用例的上下文快照占了大量存储。每条用例都存了当时的完整召回结果,几千条之后存储和加载都很吃力。修法是快照分级:核心组和事故组存完整快照(保证可精确复现),其他组只存问题文本和期望结果(复现时用当前的检索结果,接受一定的不确定性)。判据是「这条用例是否需要可精确复现」,而不是一律存全。

    踩过的坑三:批量跑评测的进度反馈不清,用户以为卡死了。几百条用例跑几分钟,界面只有一个转圈。修法是做成任务化:显示已完成条数、当前在跑哪一条、预估剩余时间,并且已完成的结果实时增量展示(不等全部跑完),用户能立刻看到有没有变差的。这和大批量操作要「逐条结果实时回显」是同一个原则——长任务不能只给一个总状态。

    没做的部分:没做用例的自动生成(用模型造测试用例)。试过,问题是生成出来的用例分布和真实用户问法差距明显,模型倾向于生成规范、完整、书面的问题,而真实用户是口语、有错别字、信息不全的。用这种用例测出来的通过率会虚高。所以边界用例仍然靠人构造,我们只用模型做一件事:帮忙把一条真实用例改写成几个变体(同义改写),扩充偏问法组。

    数字是怎么测的

    标注一致率:双标用例中两人结论相同的比例。要说清样本量和是哪一批用例——不同组的一致率差别很大(拒答组容易一致,「答得好不好」这类判断一致率低)。我们的做法是按组分别报,不给一个总的一致率,因为那个数字会掩盖真正有问题的那一组。

    评测集的增长和构成:用例总数和按组的分布,尤其是「事故驱动新增的条数」。这个数字比总数有意义——它反映的是这个机制真的在运转。还要报「跑一次全量的耗时」,因为这决定了它能不能进发布流程。

    「回归中能定位到具体改动」怎么证明:这是个能力而不是指标,用具体案例说明比给数字好。做法是描述一次真实的定位过程:某次发布前预演发现拒答组有两条变差,点进去看新旧输出,对比发现新提示词里加的「回答要简洁」让模型省掉了拒答话术里的替代路径,从发现到定位到原因用了几分钟。面试时讲这个过程比说「定位效率提升 X%」可信得多。

    断言类指标可以报精确值,判分类指标只报趋势。因为断言是确定的(引用存在性通过率就是一个准确的数),判分有波动(同一批用例重跑两次分数会有差异)。报数字时要把这个区别说出来——对判分类指标声称精确到小数点,一问「重跑一次还是这个数吗」就穿了。

    看板本身的性能:用例列表用虚拟表格,量的是几千行下的滚动帧率和首屏时间。这块和普通大表格没有区别,不是这个模块的难点,但要能答上来。

    面试追问
    Q:评测集是人工标注的,那它的标准就是标注者的主观判断,怎么保证这个标准是对的? A:不能保证「对」,只能保证「一致且可讨论」,这个区别要说清。评测集的作用是回归而不是定义正确性——它回答的是「这次改动有没有让原本的行为变差」,而不是「什么是最好的答案」。所以标准即使有主观性,只要它前后一致,就能可靠地检测出退化,这是它的主要价值。至于标准本身对不对,靠三个机制往正确的方向修:一是分歧讨论沉淀规范(两人不一致的地方就是标准模糊的地方);二是线上反馈回流(用户投诉的案例如果在评测集里是「通过」的,说明标准太松,要修标准);三是尽可能把主观判断换成客观断言——这是最根本的一条,能用代码判的就不要人判,所以我们持续在做的事情是把「答得好不好」这类模糊标准拆解成「有没有引用、数字对不对、有没有给替代路径」这些可核对的条目。拆得越细,主观性越小。
    Q:一键转用例导致评测集膨胀,你加了相似检测,那相似度阈值怎么定?定错了会漏掉真正需要的用例。 A:关键的设计决定是相似检测只做提示,不做拦截——所以阈值定错的后果是「多提示几次」或「少提示几次」,不会真的漏掉用例,因为决定权始终在人手里。这个定位让阈值不需要很准。界面上的做法是:新增时把相似的已有用例列出来并显示它们的期望结果,让人自己判断是不是重复。很多时候看到相似用例反而有价值——发现「这条我以前加过而且期望结果不一样」,说明标准有冲突,那是个更重要的问题。我倾向于把这类辅助功能都设计成「提供信息而不是做决定」,尤其是在阈值本身很难定准的时候。如果做成硬拦截,就必须把阈值调得很保守(几乎不拦),那和不做也差不多了。
    Q:这个后台是内部工具,用户可能只有十几个人,值得投入这么多工程量吗? A:值得,但要说清价值在哪,不是「因为它是工具就该做好」。它的杠杆在于它是唯一让 Agent 系统可以被安全迭代的东西。没有轨迹回放,线上问题只能靠猜;没有影响面预演,每次改提示词都是赌博;没有评测集,质量在往哪走都不知道。这三块缺失的代价不是「不方便」,是「不敢改」——团队会因为怕出问题而停止优化,那整个 Agent 系统就停在了初始水平。所以我会用「它解锁了多少次安全的迭代」来论证,而不是用「它服务多少用户」。另外从投入上说,这三个模块加起来不到两个月的工作量,而它避免的是「改提示词导致线上回归」这类事故——我们在没有它的两周里出了三次。这个对比是我当时说服排期的依据。不过我也要承认边界:如果这个 Agent 只是个内部玩具、没有真实用户、也不需要持续迭代,那这套确实过度了。投入要匹配系统的生命周期和风险等级。

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

项目拆解 · Agent 运营与调试后台(Vue)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据