Agent上线实战:评测门禁、可观测性与AgentOps全流程指南
Agent 项目从“能跑”到“能上线”中间隔着一整条工程化的鸿沟。很多人把 Agent 调好 prompt 就觉得万事大吉结果一上生产就翻车要么模型输出飘忽不定要么调用链断了没人知道要么出了问题连从哪查起都摸不着头脑。这篇东西我梳理了一套可以直接抄的 Agent 上线 Checklist 实战方案包含评测门禁怎么搭、可观测性埋点怎么设计、AgentOps 怎么落到日常流程里并且会给出真正能跑的代码片段。适合正在把 Demo 推上生产的开发者、初创团队和平台工程同学参考。1. 整体设计思路为什么 Agent 上线必须先过三道关在动手写代码之前我先聊清楚一个问题Agent 上线的难度到底在哪和传统服务有什么本质区别。传统后端服务上线你关心的是接口延迟、错误率、内存占用这些静态指标。只要服务能启动、能响应、能扛住流量基本就稳了。但 Agent 不一样它的核心逻辑是“模型动态决策 多步工具调用”。这就引入了几个传统服务没有的变量第一是输出的不确定性。同一个 prompt、同一套工具模型今天和明天给的答案可能就不一样甚至同一个请求重试两次结果也不同。这种不确定性意味着你不能只测“通不通”还得测“好不好、稳不稳”。第二是调用链的复杂度。一个 Agent 处理一个用户问题可能要先检索知识库、再调用外部 API、再让模型做一轮推理、最后生成答案。中间任何一环出问题最终结果都会跑偏而且问题可能不在最明显的那个环节。第三是回归风险。Agent 的系统提示词和工具定义是动态演化的。今天加了一个工具明天改了一个指令词可能对旧场景没什么影响但对某些边缘场景就是致命的。这种回归很难靠人工点几下来发现。所以我把 Agent 上线的流程拆成三道关对应三个核心能力评测门禁每次改完代码、prompt 或工具定义后跑一批测试用例用量化指标判断这版能不能放行。这是“准入”的问题。可观测性埋点上线之后你能看到每个请求内部发生了什么模型的每次决策、每一步工具调用、每一条 token 消耗。这是“监控”的问题。AgentOps 落地把评测、部署、监控、告警、复盘串成一条流水线让整个 Agent 生命周期是可控、可回滚、可追溯的。这是“流程”的问题。这三者是递进关系缺一不可。没有评测门禁你不敢改东西没有观测埋点线上出了问题你查不到原因没有 AgentOps 流程评测和观测都是散落的脚本发挥不了作用。接下来我按这三块分别展开每一块都会给出我实际用过的方案和能跑通的代码。2. 评测门禁实战用可量化的门槛卡住每一次变更2.1 评测门禁的设计原则评测门禁的核心不是“测一测”而是“卡一卡”。它要回答的问题是这一版 Agent 到底能不能上线。我见过很多团队的做法是把测试用例跑一遍看一眼通过率差不多就放了。这其实埋了一个大坑Agent 的测试结果本身就是有波动的今天通过率 90%明天同一个版本没动代码通过率可能就掉到 85%。如果你只设一个固定阈值很容易出现“昨天行今天不行”的随机性误杀或者“阈值设太松导致烂版本混上线”的漏网。我的门禁设计原则有三条第一条用例要分层。不是所有测试用例的权重都一样。核心场景比如主流程链路、支付环节必须 100% 通过普通功能可以有少量容错边缘场景比如超长输入、极端措辞允许一定的失败率。这样既能守住底线又不会因为边缘情况过度阻塞迭代。第二条指标要多维度。只看“通过率”远远不够。我通常会同时看成功率、关键环节的调用次数、平均延迟、Token 消耗量、对指定句式的敏感度变化。这里面特别容易忽略的是 Token 消耗——有时候模型改了一版指令答案看起来差不多但 Token 消耗涨了 30%这在线上的成本影响是实打实的。第三条结果要可追踪。每次评测跑完必须留下记录跑了哪些用例、模型用的哪个版本、prompt 是哪个 hash、工具定义是什么样的。这样后续才能做回归对比出了问题也能追溯到具体是哪次变更引入的。2.2 具体代码实现一个轻量级评测门禁我用的方案是 Python 脚本 一个简单的 JSON 结果汇总。不依赖重型评测框架方便你直接移植进自己的 CI 流程。核心逻辑分成三步加载用例集、执行测试、计算指标并给出放行结论。先看用例集的结构# eval_cases.py EVAL_CASES [ { id: core_001, category: core, # core / normal / edge input: 帮我查一下本周的订单统计并生成一份简报, expected_steps: [search_orders, generate_report], expected_keywords: [订单, 周报], max_tokens: 2000, timeout: 30, }, { id: normal_001, category: normal, input: 把昨天那个客户的合同状态改一下, expected_steps: [get_customer, update_contract], expected_keywords: [已更新], max_tokens: 1500, timeout: 20, }, { id: edge_001, category: edge, input: , expected_steps: [], expected_keywords: [请输入问题], max_tokens: 500, timeout: 10, }, ]注意这里的expected_steps我用来校验 Agent 的工具调用路径是否正确。这个比只看最终输出更能反映 Agent 的决策质量。比如用户要求查订单并生成简报如果 Agent 只做了查询没做报告生成最终输出可能也能凑合蒙混过关但步骤校验会直接把它打回。然后是执行器import json import time import hashlib from agent_core import run_agent # 假设这是你的 Agent 入口函数 def compute_hash(obj): raw json.dumps(obj, sort_keysTrue, ensure_asciiFalse) return hashlib.sha256(raw.encode()).hexdigest()[:12] def run_single_case(case, agent_version): start time.time() try: # 这里 agent_version 是为了追踪用的是哪个版本 result run_agent( case[input], timeoutcase[timeout], trace_idfeval-{case[id]}-{agent_version} ) elapsed time.time() - start return { case_id: case[id], category: case[category], success: True, output: result.get(output, ), steps: result.get(steps, []), tokens: result.get(tokens_used, 0), latency: round(elapsed, 2), } except Exception as e: return { case_id: case[id], category: case[category], success: False, error: str(e), latency: round(time.time() - start, 2), }在检查步骤的时候我用了集合包含判断而不要求顺序完全一致。因为 Agent 有时候会用并行调用的方式步骤顺序会变但最终逻辑是等价的。下面是门禁判断的核心逻辑def evaluate(cases, agent_version): results [run_single_case(c, agent_version) for c in cases] # 按类别聚合 summary {core: {pass: 0, total: 0}, normal: {pass: 0, total: 0}, edge: {pass: 0, total: 0}} total_tokens 0 total_latency 0 for r in results: cat r[category] summary[cat][total] 1 if r[success] and check_expected(r): summary[cat][pass] 1 total_tokens r[tokens] total_latency r[latency] # 门禁判定 core_rate summary[core][pass] / max(summary[core][total], 1) normal_rate summary[normal][pass] / max(summary[normal][total], 1) edge_rate summary[edge][pass] / max(summary[edge][total], 1) gate_passed ( core_rate 1.0 and # 核心 100% normal_rate 0.9 and # 普通 90% edge_rate 0.7 and # 边缘 70% total_tokens 100000 # 总量上限防暴涨 ) report { agent_version: agent_version, gate_passed: gate_passed, summary: summary, total_tokens: total_tokens, avg_latency: round(total_latency / max(len(results), 1), 2), cases: results, } # 写入评测记录 with open(feval_reports/{agent_version}.json, w) as f: json.dump(report, f, ensure_asciiFalse, indent2) return gate_passed, report2.3 门禁阈值怎么定不只是拍脑袋第一次搭评测门禁的时候我定阈值基本靠猜上线之后就被现实上了一课。现在我的做法是先用旧版本跑三到五次收集数据再按“三个九、两个八、一个七”的原则来设初始值核心场景通过率要求 100%这个没得商量。核心场景对应的就是线上不可失败的用户旅程。普通功能通过率至少 90%。留 10% 的容错空间给模型的不确定性和 prompt 的微调留缓冲。边缘场景通过率至少 70%。边缘场景本身就是低概率、高刁钻度的输入模型不完美是常态。Token 总消耗上限我设的是不超过基线版本的 120%。意思是如果旧版本跑完整套测试用例消耗 10 万 token新版本如果超过了 12 万即使答案都对也会被门禁拦下来。因为这种 Token 膨胀往往意味着 prompt 变得啰嗦、推理路径变长或者触发了不必要的工具调用短期内看似没事长期下来成本会让你崩溃。这里有一个非常重要的实操细节评测用例集本身也要纳入版本管理。我见过不少团队 prompt 改得很勤但用例集一两个月不更新导致门禁渐渐形同虚设。我会在每次上线前强制过一遍用例集看有没有新发现的线上问题值得固化成测试用例。线上每次出了典型事故对应的场景都要加进用例集里这就是“事故驱动评测”的思路。3. 可观测性埋点实战把 Agent 的内部决策过程变成可视化数据3.1 埋点到底要埋哪些东西很多人一听到可观测性就想到 Prometheus 指标但 Agent 的观测比传统服务要复杂一层。你不仅要看“接口响应慢”还得回答“为什么慢”——是模型推理慢、还是工具调用卡住了、还是 Agent 在多个工具之间反复横跳我总结了一套 Agent 埋点的最小维度集请求维trace_id、session_id、用户标识、输入摘要千万注意脱敏、进入时间。模型维模型名称、prompt 版本号、temperature 参数、每次推理的输入/输出 token 数、推理耗时。工具维每个工具的名称、入参、出参摘要、调用耗时、是否成功、重试次数。决策维每轮循环里 Agent 选择了哪个工具、为什么选择可以记录模型输出的 thinking 片段、执行完之后的下一步意图。这里最难做好的不是埋点代码本身而是“把链路串起来”。Agent 内部是多轮循环的每轮循环里模型可能要决策、调工具、再决策。要完整还原这个过程你的埋点必须带层级结构一次请求包含多次模型调用每次模型调用包含一个工具执行工具执行里又有具体的外部 API 请求。我用的是 OpenTelemetry 的 Trace Span 模型来做这件事。在 OpenTelemetry 的框架下一次 Agent 请求就是一个 Trace里面的每个模型推理和工具调用都是一个 Span通过 parent-child 关系形成一棵调用树。这样既能在 Jaeger 或 Grafana Tempo 里看到全貌也能在出问题时聚焦到某个具体环节。3.2 实现代码Agent 内的埋点工具类为了让代码可运行我提供了一个最小实现。它不依赖完整的 OpenTelemetry SDK而是用字典结构把链路记录下来方便你理解逻辑后自行接入任意监控系统。import time import uuid from contextlib import contextmanager from collections import defaultdict class AgentTracer: def __init__(self, trace_idNone): self.trace_id trace_id or str(uuid.uuid4()) self.spans [] self.span_stack [] self.span_counter 0 def start_span(self, name, span_type, attributesNone): span_id f{self.trace_id}-{self.span_counter} self.span_counter 1 parent_id self.span_stack[-1][span_id] if self.span_stack else None span { span_id: span_id, parent_id: parent_id, name: name, type: span_type, # model_inference, tool_call, agent_loop, external_api attributes: attributes or {}, start_time: time.time(), end_time: None, status: running, } self.spans.append(span) self.span_stack.append(span) return span def end_span(self, statusok, extra_attrsNone): if not self.span_stack: return span self.span_stack.pop() span[end_time] time.time() span[status] status span[duration_ms] round((span[end_time] - span[start_time]) * 1000, 2) if extra_attrs: span[attributes].update(extra_attrs) contextmanager def span(self, name, span_type, attributesNone): self.start_span(name, span_type, attributes) try: yield self.end_span(ok) except Exception as e: self.end_span(error, {error: str(e)}) raise def export(self): return { trace_id: self.trace_id, spans: self.spans, total_duration_ms: round((time.time() - self.spans[0][start_time]) * 1000, 2) if self.spans else 0, }使用的时候你在 Agent 的主循环里这样挂载def run_agent_with_tracing(user_input, tracerNone): tracer tracer or AgentTracer() with tracer.span(agent_main, agent_loop, {input: user_input[:50]}): current_input user_input for step in range(MAX_STEPS): # 模型推理步骤 with tracer.span(model_inference, model_inference, { model: gpt-4o, prompt_version: v20250601, temperature: 0.2, }) as inf_span: output call_model(current_input) # 你的模型调用 inf_span[attributes][tokens_used] output.get(usage, 0) inf_span[attributes][thinking] output.get(thinking, )[:200] # 决策是否调用工具 tool_name parse_tool_called(output) if not tool_name: break # 工具调用步骤 with tracer.span(ftool_call_{tool_name}, tool_call, { tool_name: tool_name, arguments: extract_args(output)[:100], }) as tool_span: result call_tool(tool_name, extract_args(output)) tool_span[attributes][result_summary] str(result)[:200] current_input f{previous_output}\n工具结果: {result} return tracer埋点数据导出之后你在 Jaeger 里就能看到一棵完整的决策树。我实际排查过的一个线上问题就是靠这个工具定位到的用户反馈“Agent 回答越来越慢”看指标发现整体延迟从 2 秒涨到了 8 秒。一开始以为是模型变慢了通过 span 树看到模型推理只多了 100 毫秒真正慢的是工具调用里嵌套的外部 API——某个上游接口从 200ms 涨到了 6 秒。如果没有这棵树排查方向必然跑偏。3.3 埋点数据要汇总成四个核心面板埋点只是手段最终目的是让你“一眼看出异常”。我把埋点数据汇总成四个看板推荐你也照着搭健康总览请求量、成功率、平均延迟、P95 延迟、Token 消耗总量。工具链路分析每个工具的调用次数、失败率、平均耗时。重点看有无工具被异常高频调用——我曾遇到过一次死循环 bug就是 Agent 反复调用同一个查询工具 12 次才放弃靠这个面板发现的。模型行为分布各模型的调用占比、单次推理 token 数分布、推理耗时的热点区间。异常追踪错误类型分组、跟踪堆栈、涉及的用户会话。出现新错误类型就要触发告警。这四个面板听起来工作量大但如果你埋点格式统一构建 Dashboard 只花半天时间。数据落到 Elasticsearch 或 ClickHouse 之后用 Grafana 直接配图表即可。4. AgentOps 落地实战把评测和观测变成发布流水线的一部分4.1 从“手工部署”到“环境门禁流水线”评测门禁和可观测性埋点搭好之后真正让它们发挥价值的是融进发布流水线。如果你还是每次手动跑测试、手动部署、出问题手动回滚那前面搭的所有东西都会慢慢荒废。我推荐的 Agent 发布流水线是这样设计的开发环境dev本地代码改动后自动构建构建完成后自动跑“冒烟评测子集”比如核心用例 10 条 普通用例 20 条只判断过与不过不强制阻断主要给开发者即时反馈。预发环境staging通过冒烟评测后部署到 staging部署完成后跑“全量评测门禁”就是第一节里的完整逻辑。只有门禁通过的构建产物才有资格申请上线。同时 staging 期间持续收集可观测性数据观察有没有异常流量或错误。生产环境prod门禁通过后进入生产灰度。先切 5% 流量运行若干小时对比核心指标是否与旧版本持平或更优再逐步放量到 30%、100%。每个阶段如果指标异常自动触发回滚。这里最重要的一点是——每一个环境要有独立 Agent 实例或者至少独立的数据上报 Tag。我见过有些团队 dev 和 prod 共用一个 Agent日志搅在一起出了问题根本分不清是线上问题还是测试干扰这个坑必须避开。4.2 自动化发布脚本Python 示例我用一个 Python 脚本把“评测门禁 部署 灰度策略”串起来。这个脚本基于 Fabric 的思路但去掉了依赖用一个简单的 subprocess 来演示。import subprocess import json import time import sys AGENT_REPO agent-service REGISTRY registry.example.com/agent def run_cmd(cmd, cwdNone): print(f[exec] {cmd}) result subprocess.run(cmd, shellTrue, capture_outputTrue, textTrue, cwdcwd) if result.returncode ! 0: raise RuntimeError(fCommand failed: {cmd}\n{result.stderr}) return result.stdout.strip() def build_and_push(version): run_cmd(fdocker build -t {REGISTRY}:{version} .) run_cmd(fdocker push {REGISTRY}:{version}) def run_eval_gate(version, fullTrue): subset --full if full else --smoke return run_cmd(fpython eval_runner.py {subset} --agent-version {version}) def deploy_to_env(env, version): run_cmd(fkubectl set image deployment/agent-{env} agent{REGISTRY}:{version} --record) run_cmd(fkubectl rollout status deployment/agent-{env} --timeout120s) def get_prod_metrics(version, minutes30): # 实际场景中从监控系统拉数据这里用模拟值 return {error_rate: 0.1, p95: 1200, token_per_req: 3500} def canary_release(version, steps[5, 30, 100]): deploy_to_env(staging, version) gate_result run_eval_gate(version, fullTrue) print(fEval gate passed: {gate_result}) # 切流到生产 for percent in steps: run_cmd(fkubectl scale deployment/agent-prod --replicas5) # 基础实例 run_cmd(fpython traffic_shift.py --percent {percent} --from agent-old --to agent-new) time.sleep(60 * 5) # 每个阶段观察5分钟 metrics get_prod_metrics(version) if metrics[error_rate] 0.5: run_cmd(fpython traffic_shift.py --percent 0 --from agent-new --to agent-old) print(Canary failed, rolled back!) sys.exit(1) print(fCanary {percent}% healthy: {metrics}) if __name__ __main__: version time.strftime(%Y%m%d%H%M%S) build_and_push(version) run_eval_gate(version, fullFalse) deploy_to_env(dev, version) canary_release(version)这个脚本的核心思路是版本号 流水线步骤 可追踪。每次发布都会生成一个新的镜像版本号所有评测记录和监控数据都挂在这个版本号下。一旦生产出了问题你可以直接把流量切回上一个版本号同时根据版本号拉出评测记录判断是评测漏掉了还是环境因素导致的。注意灰度切流我用了traffic_shift.py占位实际场景中用 Kong、APISIX 或 Kubernetes Service Mesh 的流量权重即可。4.3 回滚和告警是 AgentOps 的“兜底”能力Agent 出问题的概率比传统服务高得多而且出错方式很诡异。有一种经典的场景模型输出正常工具调用成功但生成的结果里引用了错误的知识库片段导致用户收到误导性信息。这种问题评测门禁很难全部拦截可观测性只能事后定位真正能起到保护作用的其实是回滚机制和告警规则。我把 Agent 告警规则分成三个级别供你参考紧急告警立即出发错误率连续 5 分钟超过 2%、P95 延迟超过 3 秒、工具调用失败率超过 15%。这些说明服务已经实质不可用需要立刻介入。严重告警10 分钟内Token 消耗环比突增 50%、调用链中出现未知错误类型、某个工具的调用频率超过基线的 5 倍。这类异常不会立刻让系统挂掉但可能预示逻辑问题正在酝酿。提示告警汇总每日模型决策分布发生明显偏移、某类输入的驳回率上升、平均推理轮数增加。这类多属于行为漂移定期看报告即可。回滚机制上我强烈建议部署 Agent 时保留至少最近 3 个版本的镜像。有一次我的 Agent 在预发环境测了半个月没出问题上线第 3 天突然出现大面积错误原因竟然是某外部 API 更新了返回格式而新版 Agent 和旧版解析方式不一样。由于我保留了旧版镜像10 分钟就完成了回滚避免了更大范围的影响。如果你的 Agent 上线前就把旧镜像清理掉遇到这种问题就只能干瞪眼。5. 常见问题与排查技巧实录5.1 评测门禁形同虚设用例集没有跟上业务变化我接手过一个项目团队设置了 80 条评测用例但业务侧已经迭代了两个月用例集一条没加。新增了不少工具调用旧用例只覆盖老三样门禁测出来的通过率很高上线照样出问题。排查下来发现新工具的调用路径根本没有用例覆盖。我的做法是建立一个“线上事故 → 评测用例”的正向循环。每次线上出了问题或者用户投诉第一件事就是把这个场景固化成一个新的评测用例并且标注其优先级和类别。然后门禁跑一次确保修复后的版本能通过这个用例才算闭环。坚持一个月之后你的用例集会越来越逼近真实的用户分布。5.2 观测到了链路但看不懂链路缺少“预期行为”对照有段时间我搭好了完整的埋点但是排查问题效率还是很低。原因是只看 Span 树很难判断“哪个环节不正常”——有些环节慢是正常波动有些是隐患。直到我把观测面板叠加了“基线对比”功能问题才迎刃而解。具体做法是从历史数据中统计过去 7 天的 P50 和 P95 值作为每个工具调用的基线。如果当前某个工具调用的耗时超过了基线 P95就在面板上标红。这样你不需要凭直觉判断哪里异常系统会帮你把偏离正常范围的环节标出来。埋点的价值不在于记录数据而在于把数据和预期值对齐快速暴露偏差。5.3 Agent 有“记忆污染”问题上线前必须清理上下文这是个很隐蔽的坑。评测门禁测试时Agent 会带着先前测试用例的上下文某些 Agent 架构会把对话历史存到内存或向量库里。如果清理不彻底生产环境的 Agent 可能“想起”测试阶段的内容导致输出混乱。我的方案是每次测试和部署都强制刷新 Agent 的会话状态。评测跑完后清空对话缓存发布到生产时再清一遍。这个操作在代码里就三行但不写的话线上 Agent 的行为就是随机的。另外合理使用session_id也是关键——生产环境每个用户应该用独立的 session_id这样不同用户的上下文不会互相污染。5.4 Token 经济评估缺位功能能跑但成本失控最容易被忽视的上线指标是成本和收益的匹配。Agent 相对传统接口单次请求成本要高得多。尤其是 RAG 场景单次请求可能消耗几千到几万 token而其中大量 token 消耗在冗余的上下文拼接上。我的建议是评测门禁里不仅要卡“是否通过”还要报告“单次请求 token 成本”。每次全量评测跑完直接换算成对应模型的价格输出一份“版本 token 成本对比表”。比如上一版基准成本是 0.8 元/请求新版本如果是 1.2 元/请求即使功能全部通过也要评估这多出来的 0.4 元是否换来足够的质量提升。没有这个指标你的 Agent 可能会在你没注意到的情况下把一个盈利业务拖成亏本生意。6. 再分享一个关于这套流程落地节奏的经验最后聊一点实际的。这套评测门禁 可观测性 AgentOps 的体系不要试图一天全部搭完。我自己的推进顺序是先接埋点后加门禁最后串流水线。先接埋点是因为没有观测数据你根本不知道自己有哪些问题。跑一周埋点之后你会清楚地看到哪些工具调用最不稳定、哪些场景最容易触发多轮循环、token 消耗都集中在哪。有了这些数据再设计评测门禁的用例集和阈值才能有的放矢。而有了门禁之后再把它塞进发布流水线才不会被团队抵触——因为门禁拦截的确实是过去一周观测到的真实问题而不是拍脑袋定出来的规则。如果你现在正处于 Agent 项目“能跑但不敢上线”的阶段建议从今天起就把埋点加上去。不用一上来就上完整的 OpenTelemetry 全家桶用一个像我上面写的 AgentTracer 类把每次请求的工具调用链记录下来存成 JSON 就行。等你跑出一周的数据之后再回头看评测门禁怎么设阈值心里就有谱了。工具再多不如先让数据说话。