第一版就是「把后端返回的轨迹 JSON 全部渲染出来」,一个可折叠的树形结构。功能上没错,但实际用起来三个问题让它几乎没人用。
一是打不开。一次十几轮的会话,每轮的输入都包含完整的系统提示、对话历史、检索到的文档全文,整个 JSON 有几 MB。一次性拉取加渲染,页面卡死好几秒,偶尔直接崩。而排查问题的场景往往是线上告警之后,工程师最需要它快的时候它最慢。
二是看不出问题在哪。十几轮内容平铺出来,每轮的输入有 90% 是和上一轮重复的(系统提示、大部分历史都一样)。工程师要在大量重复内容里找那 10% 的变化,实际操作是把两轮的 JSON 复制出来丢进 diff 工具——那说明这个界面没有提供它该提供的价值。
三是找不到关键事件。「哪一轮的工具报错了」「哪一轮检索是空的」这些是排查的入口,但它们埋在树形结构的深处,要一层层展开才能看到。
所以第二版重新定位:这个界面的目标不是「展示轨迹」,是「定位到出错的那一轮」。三个改动对应三个问题:索引与正文分离、按轮次和大对象两级懒加载(解决打不开)、相邻轮次输入做结构化 diff(解决看不出)、关键事件打断点标记并可跳转(解决找不到)。
最值得说的判断是第二条:轨迹回放的核心视图不是「每轮的完整内容」,而是「每轮相对上一轮变了什么」。因为 Agent 出错的原因几乎总是「这一轮新进来的东西」——新的检索结果、新的工具返回、被裁剪掉的历史。把不变的部分默认收起、只呈现变化,信息密度提升了一个量级,而这个改动纯粹是前端的视图设计,不需要后端配合。
diff 选了结构化而不是文本 diff,代价是要为每种内容类型写比对逻辑。文本 diff 库现成、几行就能接上,但它对 JSON 的效果很差——字段顺序变化、格式化差异、长文本内部的换行都会被算成变化,真正的语义变化被噪音淹掉。结构化 diff 要为检索片段、历史轮次、工具返回各写一套比对规则,工作量明显更大。但这个模块的全部价值就在于「让变化一眼可见」,diff 做不准整个模块就没意义了。取舍的判据是「这部分是不是这个功能的核心价值」——是核心就不要用现成的通用方案凑。
两级懒加载 vs 一次拉完再前端处理。如果一次拉完,前端可以做得更流畅(切换轮次零延迟)。但轨迹体积决定了这不可行——几 MB 的传输和解析在告警时段是不可接受的。代价是切换轮次有网络延迟,我们用两个手段缓解:预取相邻轮次(看第 5 轮时后台预拉第 4 和第 6 轮,因为排查时前后翻是最常见的操作)、以及已加载的缓存不释放(在上限内)。这个组合下实际体验接近本地切换。
「重跑这一轮」放在前端做还是后端做,我们选了后端提供沙箱接口、前端只负责发起和展示对比。前端不碰模型调用,避免密钥暴露和成本失控。关键约束是沙箱必须禁用有副作用的工具——重跑一个包含「发优惠券」的轮次,不能真的再发一张。这一点是前端提出来的需求,因为最初的实现真的重复发了券。
踩过的坑一:虚拟滚动用在了不该用的地方。一开始把「完整输入」这种长文本也做了虚拟滚动,以为能提升性能。结果是浏览器的原生搜索(Ctrl+F)失效了——虚拟滚动只渲染可见区域,搜不到未渲染的内容。而工程师查轨迹最常用的操作恰恰是全文搜索一个订单号。修法是长文本改为「折叠 + 完整渲染」:默认折叠不占性能,展开后完整渲染保证可搜索。这个判断和报表页选 DOM 不选 Canvas 是同一类考虑——要先问清用户会对这块内容做什么操作。
踩过的坑二:时间线的事件标记设计了十几种,没人看得懂。不同颜色不同图标,用户需要对照图例才能理解。修法是砍到五类,并且用文字标签而不是纯图标。删掉的那些不是不重要,而是「知道了也不会改变下一步动作」,那就不该占据视觉资源。
没做的部分:没做多会话的对比视图(同时打开两个会话左右对照)。需求是真实的(「为什么这个用户对那个用户错」),但界面复杂度上升很多,而且实际排查中更常用的是「同一会话的轮次间对比」,这个已经做了。把它列为已知需求但明确没做,比硬做一个半成品好。
首屏数据量:用浏览器网络面板量首屏请求的总传输字节,对比改造前后同一条轨迹。要说清测的是哪条轨迹——轨迹体积差异很大,用「一条十几轮的典型会话」这种描述,并且说明轮数,否则数字不可比。我们报的是「降到原来的一个小比例」而不是精确倍数,因为它取决于会话长度。
渲染耗时:用 Performance 面板量从请求返回到时间线可交互的时长,取多次的中位数。关键是要量「可交互」而不是「渲染完成」——用户能点第一个节点就算可用了,正文还在加载不影响。这个区分让指标和真实体验对齐。
定位效率用操作步数而不是时间。做法是给几个真实的历史问题,让工程师用新旧两版界面各定位一遍,记录点击和展开的次数。用步数不用时间的理由是:时间受熟练度和问题难度影响太大,样本又少(我们只找了三四个同事试),步数是更客观的代理指标。报法是「操作步数明显减少」加上具体的对比场景描述,不编精确百分比。
内存占用:连续查看二十轮之后看 Performance 里的堆快照,确认缓存上限生效、没有持续增长。这个必须测,因为轨迹正文很大,缓存不设上限的话查几个会话就能吃掉几百 MB。
diff 正确性用构造用例。造几组轨迹(新增检索片段、裁剪历史、系统提示变更、工具返回变化),断言 diff 结果准确标出了变化项且没有误报。误报比漏报更该重点测——一个到处标着「变化」的 diff 视图会让人立刻不信任它。
第一版是个很朴素的表单:一个大文本框,改完点保存,立刻全量生效。这个界面在两周内引发了三次线上问题。
一是改一处坏另一处。运营为了让回答更简洁,在提示词里加了一句「回答控制在三句话以内」。上线后简洁了,但模型把必要的免责说明也删掉了,涉及退款政策的回答变得不完整。而这个副作用在改的时候完全预料不到,是客户投诉才发现的。
二是变量占位符写错,静默失效。提示词里有 {userName} 这类占位符,有人手打成了 {userNmae}。渲染时占位符没被替换,原样进了模型的输入,模型看到一个奇怪的花括号内容,行为变得不稳定。没有任何报错,排查了半天。
三是两个人同时改,后保存的覆盖了前面的。运营在调话术、工程师在改工具描述,两人打开的是同一份,谁后点保存谁的版本生效,另一个人的改动无声消失。
更根本的问题是:大家不把「改提示词」当成一次发布。它只是「改了几句话」,所以不评审、不测试、不灰度、直接全量。但它的影响面比大部分代码改动都大——它是全局生效的,同时影响所有场景。
所以第二版的核心思路是把提示词当代码对待,并且把「测试」这一步做进发布界面本身。三个改动:发布前自动在评测集上预演并展示变差用例(解决改一处坏另一处)、提交前做占位符结构校验(解决静默失效)、乐观锁加变更提示(解决覆盖)。
最值得说的判断是第一条:这个界面要给的不是文本差异,是「这次改动会让哪些用例从通过变失败」。文本 diff 只能告诉你改了什么字,而提示词的改动和效果之间没有直观的对应关系——加一句话可能影响一个完全没想到的场景。所以真正有决策价值的信息是评测结果的差异,而不是文本的差异。把这个信息放在发布按钮旁边,人才会在按之前看一眼。
预演做成阻断式还是提示式,我们选了「分组差别对待」。全部阻断会让流程太重——评测集里有些用例本身是难的边界情况,偶尔失败是正常的,如果任何变差都阻断,运营会永远发不出去,最后一定会想办法绕过整个机制。全部只提示则等于没有约束。所以按用例的性质分:历史事故组硬阻断(不允许强制发布),其他组允许强制发布但必须填写理由并记入发布记录。这个设计的关键是「让绕过变得可见而不是变得不可能」——完全堵死会催生绕过机制的行为,可见的绕过反而能被管理。
diff 选按语义单元做,代价是要维护段落切分规则。提示词的段落边界不像代码那么明确,我们的做法是要求提示词用固定的分段标记组织(每个功能段有标题),这样切分就是确定的。这相当于给提示词加了一个轻量的结构约束。代价是运营写的时候要遵守格式,收益是 diff 准确、影响面清楚、而且强迫大家把提示词写得有结构而不是一大段流水账。这个约束后来被证明比 diff 本身更有价值。
编辑器用 Monaco 而不是普通 textarea。需要的是语法高亮(占位符)、自动补全(可用变量)、以及和 diff 视图的一致体验。代价是包体积明显增加,所以做了路由级懒加载,只有进这个页面才加载。这个取舍在内部工具上是划算的——使用者是每天要改十几次的运营,编辑体验的收益远大于首次加载慢半秒。如果是给终端用户的低频功能,我不会引入它。
踩过的坑一:预演跑得太慢,没人愿意等。评测集有几百条用例,串行跑一遍要几分钟,运营点了预演就去干别的了,回来忘了看结果直接点发布。修法是两条:并发跑(受限于模型服务的限流,我们设了并发上限)、以及分层跑——先跑历史事故组和核心组(几十条,几十秒出结果),这批过了就允许发布,全量集在后台继续跑完再补充报告。关键的判断是:阻断性检查必须快到人愿意等,否则它会被绕过。
踩过的坑二:diff 视图默认展开全部,改一个字看到满屏绿色。因为提示词很长,即使只改了一句,整个文档都渲染在 diff 视图里,用户要滚很久才找到改动处。修法是默认只显示有变化的段落,未变化的段落折叠成一行「N 段未变化」。这和轨迹回放那边「只呈现变化」是同一个原则,两个模块都是这个思路。
踩过的坑三:乐观锁冲突的提示太粗糙。最初只显示「版本冲突,请刷新后重试」,用户刷新之后自己的改动全丢了,得重新写一遍。修法是冲突时把三方内容都展示出来:你的版本、对方的版本、共同的基线,让用户自己选择合并。「冲突提示必须保住用户已经付出的劳动」,这是这类协同场景的基本要求。
没做的部分:没做提示词的 A/B 效果自动判定(自动决定哪个版本更好然后全量)。技术上可以做,但「哪个版本更好」在很多情况下不是单一指标能判定的——简洁度上去了但完整性下来了,谁更好取决于业务当下的优先级。这个判断应该由人做,系统的职责是把数据摆清楚。我们只做到了展示对照数据和一键切换。
「发布前拦截」的效果怎么证明:统计预演阶段被判定为变差、因此没有发布出去的次数,以及其中被人工确认「确实是问题」的比例。后一个数字很重要——如果拦下来的大部分是误判(评测标准太严),那这个机制只是在添麻烦。我们的做法是每次拦截都要求填「是真问题还是误判」,用这个数据反过来校准评测集。
预演耗时按分层报。核心组(几十条)的耗时是阻断性的,必须报;全量集的耗时是参考。要说明并发数和是否受模型服务限流,否则这个数字不可比——同样的用例数,并发 5 和并发 20 差好几倍。
占位符校验的价值用「拦下的次数」衡量。上线后统计提交时被校验拦住的次数和类型分布(拼错变量名 / 未闭合 / 声明未使用)。这是个直接的、无争议的计数,比任何间接指标都清楚。而且它能反过来说明这类错误有多高频——我们的数据里拼错变量名占了大头,印证了自动补全的必要性。
回滚耗时:从点回滚到新配置生效的端到端时长。这个指标的意义是「出事之后多久能止损」,所以要量的是真实的生效时间(含配置下发和缓存刷新),不是接口响应时间。我们的做法是回滚后立刻发一个探测请求确认生效版本。
编辑器的加载耗时:因为引入了 Monaco,要单独量这个页面的首屏时间并和其他页面对比。报的时候要说明做了路由级懒加载,否则面试官会问「引入这么大的编辑器不影响性能吗」——主动说清缓解手段。
第一版的评测集是一个 CSV 文件,工程师手动维护,跑评测是本地跑个脚本看输出。三个问题让它形同虚设。
一是评测集不增长。维护它是额外工作,没人有动力做。线上出了问题,大家忙着修,修完就过去了,那个宝贵的失败案例没有被留下来。三个月后同类问题复发,才发现从来没有把它加进评测。
二是里面全是正常用例。最初的用例是从日志里挑「典型问题」,全是正常场景,任何改动跑起来都接近满分,等于没测。真正有价值的是那些会失败的、边界的、有诱导性的用例,但这些不会自然出现在「典型问题」里。
三是标注不一致。两个人标同一条用例,对「这个回答算不算通过」的判断不同——一个觉得答案不够完整算失败,一个觉得核心信息有了算通过。标注标准不统一,评测结果就没有可比性,跑出来的通过率是个噪音数字。
还有个更隐蔽的问题:把代码断言和模型判分混在一起算一个总分。「有没有引用来源」是确定的(代码判,一定准),「回答是否恰当」是模型判的(有偏差,会偏好长答案)。混成一个总分之后,这个分数既不精确也不可解释——分数降了不知道是真的变差还是判分模型抽风。
所以第三版重做了四件事:把创建用例的入口放到轨迹页(在最有动力的时刻,也就是刚排查完问题的时候,一键就能留下来)、用例分组并显式区分失败与边界类、双人独立标注加分歧比对、断言与判分分栏展示不合并总分。
最值得说的判断是第一条:评测集能不能持续增长,取决于「添加用例」这个动作发生在什么时刻。做成一个独立的管理页面,就永远需要额外的自觉;做成轨迹回放页上的一个按钮,它就发生在工程师刚刚查完问题、上下文最清楚、最想让这个问题别再犯的那一刻。这个设计上的位置选择,比界面本身做得多好都重要。
双人标注的成本很高,我们只对部分用例做。全部用例双标,标注工作量直接翻倍,实际推不动。取舍是:新建用例和存在分歧历史的用例双标,其余单标。另外每个季度抽一批已有用例重新双标,用来检查标准有没有漂移。这个方案承认了「一致性保障是有成本的、只能用在最需要的地方」,比声称全量双标要真实——面试时如果说「我们所有用例都双人标注」,一问标注工时就穿了。
模型判分留下来了,没有因为它有偏差就砍掉。它的价值在于能覆盖代码断言判不了的维度(表述是否清楚、是否答到了点上)。处理偏差的方式不是放弃它,而是限制它的用途:只作参考不用于阻断、判分标准拆成具体条目、以及固定判分模型的版本(判分模型自己升级会让历史分数不可比,这个坑很容易忽略)。
看板选了「三分归因」而不是行业常见的「通过率趋势」。通过率是个更简洁的指标,管理层也更喜欢看。但它的问题是掩盖了此消彼长——修好三个弄坏两个,通过率是涨的。我们的妥协是顶部仍然给通过率(对上汇报用),但默认展开的是三分明细(干活用)。两个受众要的东西不同,界面上要同时满足,而不是二选一。
踩过的坑一:一键转用例做得太顺,评测集迅速膨胀到跑不动。入口做好之后大家很愿意用,几个月加了上千条,全量跑一遍要很久,而且里面有大量重复的同类用例。修法是加「相似用例检测」——新增时提示「已有 3 条相似用例」,让人判断是否真的需要加;以及给用例加权重和采样策略,日常回归跑核心子集,全量集定期跑。这个坑说明「让某个行为变容易」之后必须同时考虑它的量的治理。
踩过的坑二:用例的上下文快照占了大量存储。每条用例都存了当时的完整召回结果,几千条之后存储和加载都很吃力。修法是快照分级:核心组和事故组存完整快照(保证可精确复现),其他组只存问题文本和期望结果(复现时用当前的检索结果,接受一定的不确定性)。判据是「这条用例是否需要可精确复现」,而不是一律存全。
踩过的坑三:批量跑评测的进度反馈不清,用户以为卡死了。几百条用例跑几分钟,界面只有一个转圈。修法是做成任务化:显示已完成条数、当前在跑哪一条、预估剩余时间,并且已完成的结果实时增量展示(不等全部跑完),用户能立刻看到有没有变差的。这和大批量操作要「逐条结果实时回显」是同一个原则——长任务不能只给一个总状态。
没做的部分:没做用例的自动生成(用模型造测试用例)。试过,问题是生成出来的用例分布和真实用户问法差距明显,模型倾向于生成规范、完整、书面的问题,而真实用户是口语、有错别字、信息不全的。用这种用例测出来的通过率会虚高。所以边界用例仍然靠人构造,我们只用模型做一件事:帮忙把一条真实用例改写成几个变体(同义改写),扩充偏问法组。
标注一致率:双标用例中两人结论相同的比例。要说清样本量和是哪一批用例——不同组的一致率差别很大(拒答组容易一致,「答得好不好」这类判断一致率低)。我们的做法是按组分别报,不给一个总的一致率,因为那个数字会掩盖真正有问题的那一组。
评测集的增长和构成:报用例总数和按组的分布,尤其是「事故驱动新增的条数」。这个数字比总数有意义——它反映的是这个机制真的在运转。还要报「跑一次全量的耗时」,因为这决定了它能不能进发布流程。
「回归中能定位到具体改动」怎么证明:这是个能力而不是指标,用具体案例说明比给数字好。做法是描述一次真实的定位过程:某次发布前预演发现拒答组有两条变差,点进去看新旧输出,对比发现新提示词里加的「回答要简洁」让模型省掉了拒答话术里的替代路径,从发现到定位到原因用了几分钟。面试时讲这个过程比说「定位效率提升 X%」可信得多。
断言类指标可以报精确值,判分类指标只报趋势。因为断言是确定的(引用存在性通过率就是一个准确的数),判分有波动(同一批用例重跑两次分数会有差异)。报数字时要把这个区别说出来——对判分类指标声称精确到小数点,一问「重跑一次还是这个数吗」就穿了。
看板本身的性能:用例列表用虚拟表格,量的是几千行下的滚动帧率和首屏时间。这块和普通大表格没有区别,不是这个模块的难点,但要能答上来。
没有匹配的内容,换个关键词试试。
项目拆解 · Agent 运营与调试后台(Vue)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据