☰
Agent项目线上总翻车?用Python自动校验脚本提前拦截生产环境故障
2026/10/2 5:19:45 网站建设 项目流程

开发 Agent 相关项目的时候,谁没遇到过这种场面:本地 Demo 跑得飞起,工具调用、上下文处理、流程编排看起来全都正常,演示给同事看时数据链路也没毛病。结果一旦部署到线上,进入真实流量环境,要么任务超时、要么LLM输出格式漂移、要么并发一上来服务直接崩溃。翻车现场一多,你就知道 Demo 顺利和线上可用之间的距离,靠人工点点点根本测不出来。基于这个痛点,我用 Python 写了一套自动校验脚本,把“上线前验证”从靠运气变成了系统化检查。这套脚本不依赖特定 Agent 框架,核心思路是围绕确定性检查、并发模拟、异常注入和结果一致性校验来做验证,能在部署前拦截掉绝大多数“Demo没问题、上线就出事”的坑。

我接触过的 Agent 项目从 RAG 类问答、自动化工单处理到多工具协同的 research agent 都有,跑过不止一次这种翻车过程后,总结了一个很直接的感受:Agent 项目和传统 Web 接口的测试思路完全不同。传统接口你只要测清入参出参、状态码、异常分支就八九不离十,但 Agent 系统里 LLM 输出是不确定的、工具调用链路是动态的、上下文状态是会泄漏的。这意味着你根本没法用几条固定用例就断言系统是好的,必须有一套能系统性暴露问题的自动校验手段。这就是我做这套脚本的出发点。

1. 为什么 Demo 跑得通、一上线就翻车

1.1 Demo 是“单程票”,生产环境是“立交桥”

先说一个我常用来跟同事解释的类比。本地 Demo 就像你在空旷场地开车,路线固定、没有其他车辆、红绿灯都是绿的,你油门一踩就到终点。生产环境则是高峰期立交桥,多路车流汇入,有临时封路,有人随意变道,还有各种极端天气。你把空旷场地的驾驶经验直接套在立交桥上,翻车几乎是必然的。

具体到 Agent 系统里,Demo 环境默认是单个用户、单次会话、上下文短、工具调用顺利返回,LLM 也有充足时间思考。但生产环境往往是几十上百个用户同时发起会话,每个会话可能持续很久,上下文越积越长,工具调用可能超时甚至报错,第三方 API 时快时慢,LLM 的输出风格还可能因为 prompt 里微小差异产生变化。一个在 Demo 里只跑一两轮的链路,到了线上可能要在多轮工具调用、上下文裁剪、重试逻辑里反复横跳,任何一环出问题都会让最终结果走样。

1.2 具体差异要拆开看

我把 Demo 与生产的核心差异拆成了五个维度,这也是校验脚本设计时的核心输入:

维度Demo 环境生产环境翻车表现
并发量单用户、单请求多用户、多会话并行共享状态污染、CPU/内存飙升
数据真实度构造数据、理想返回值脏数据、大字段、缺失字段解析报错、工具调用参数异常
依赖稳定性本地服务可控第三方 API 有延迟、限流超时、重试风暴
上下文长度短、可控长会话、需要裁剪压缩遗忘关键信息、Token 超限
LLM 输出回答风格相对一致格式漂移、Token 截断下游解析失败、字段缺失

这些维度单独拿出来似乎都能应对,但叠加在一起问题就完全不可控了。比如某个 Agent 工具返回了一个超大 JSON,上游没问题,但下游解析时用了json.loads后还要做深度遍历,性能瓶颈就出现了;再比如工具调用的参数校验在 Demo 里根本没触发过,因为 Demo 里传参永远是合法的,但真实用户输入可能让你的 Agent 生成一个缺字段的调用参数,工具层立刻抛异常。这类问题靠人工手测很难覆盖,后果是部署后线上出现大量报错,而你在日志里看到的只有孤立的错误消息,根本不知道是链路哪一步出的问题。

1.3 为什么传统测试对 Agent 不适用

传统 Web 项目常用的单元测试、集成测试思路,直接搬过来测 Agent 会显得相当无力。单元测试只能测你写的纯函数,比如解析工具返回、计算 Token 数这类逻辑,但 Agent 核心是 LLM 决策链和工具调用编排,这是动态且不确定的,没法用单测固定住。集成测试如果只是 mock 掉 LLM 和工具,那测的其实是自己的 mock,不是真实链路。

所以我的做法是,用 Python 写一套偏重“行为验证”的脚本:它不 mock 掉核心组件,而是真实调用 Agent 服务和依赖工具,然后从外部角度校验输出结构、执行时间、资源消耗、错误恢复能力。这套脚本既能在开发环境全量跑,也能在生产环境灰度后当作监控手段,属于一种“黑盒探针 + 白盒断言”的混合体。

2. 自动校验脚本的整体设计与分层思路

2.1 先定边界:校验脚本要解决什么问题

动手写脚本之前,我先明确了它的边界:不做线上全量回归,不做传统意义上的接口测试,也不替代日志监控。它的目标是在发布前、灰度后这个窗口期内,用较短时间、较低成本,把 Agent 系统的高风险行为暴露出来。具体要回答四个问题:

  • 系统在多人同时使用时,功能是否依然正常?
  • 工具调用、上下文管理、LLM 输出在真实压力下是否稳定?
  • 出现异常(超时、非法输出、参数错误)后,系统能否恢复?
  • 结果质量是否满足最小可接受标准?

这四个问题确定后,脚本设计就有了主线:并发压测、异常注入、结果一致性校验、性能基线监控。每一条都可以独立成模块,最后汇总成一份校验报告。

2.2 整体结构分层

我把这套脚本分成四层,每一层解决一类问题,层与层之间通过统一的报告模型串联起来:

  • 环境探测层:检查 Agent 服务是否存活、依赖接口是否可达、部署环境参数是否正确。这是在跑任何测试前的“体检”,避免因为基础配置错误导致后续所有验证结果作废。
  • 确定性验证层:使用一批预设的标准输入,校验 Agent 返回结果的必填字段、结构类型、关键内容是否符合预期。这批输入可以简单到一句话,也可以复杂到一个多轮任务,但必须保证结果是可预期的。
  • 并发与压力验证层:模拟多用户同时发起请求,统计成功率、响应延迟、错误分布,重点排查共享状态污染、线程安全、超时配置问题。
  • 异常注入与恢复层:主动制造异常场景,比如让工具调用超时、让 LLM 返回空结果、让上游接口返回格式错误的数据,检查 Agent 的重试机制和错误处理是否有效。

这套结构看起来像是压测加功能测试的组合,但它和传统压测有明显区别:传统压测关心吞吐量和响应时间,这套脚本更关心业务语义的正确性。同样是 100 个并发请求,传统压测可能只看成功率 99% 就认为达标,但这套脚本会进一步检查这 100 个请求返回的内容是不是符合 Agent 应该有的行为,是不是有上下文串号、工具参数越权、引用内容张冠李戴这类“功能正确但语义错误”的隐患。

2.3 为什么选 Python 而不是现成压测工具

很多人会问,并发压测直接用 Locust、JMeter 不就行了吗?为什么还要自己写?我承认现成工具在单纯压测场景下确实省事,但 Agent 系统的校验远不是发请求、记耗时这么简单。你需要能自定义复杂的请求体结构,需要针对不同响应内容做语义断言,需要在压测的同时注入异常,还要能把多轮会话状态串起来。这些场景用现成工具配置起来成本太高,而用 Python 写一个轻量脚本反而更灵活。

再加上团队里已经有 Python 的技术积累,脚本不引入额外重依赖,直接使用aiohttp、asyncio、pydantic这类常用库,就能把整个链路打通。它在本地能直接运行,打包成 Docker 镜像后在 CI 环境里也能跑,可移植性相当好。

3. 核心校验脚本的实现细节

3.1 环境探测:先确认你没在错误的环境里跑测试

在做任何测试之前,环境探测是第一步。我这里说的环境探测,不只是检查服务端口通不通,还包括配置校验、依赖版本核对、沙盒权限确认。

一个常见的情况是:Agent 服务在测试环境跑得欢,但生产环境配置里缺少了某个环境变量,比如外部工具服务的 API Key 没配全,服务虽然能启动,但一调用工具就报鉴权错误。如果校验脚本没有环境探测,你后来的并发压测和异常注入都会在这一类错误上浪费大量时间。

我在脚本里写了一个简单的环境检查模块,核心逻辑是:

import os import asyncio import aiohttp REQUIRED_ENV = [ "AGENT_API_BASE", "AGENT_API_KEY", "TOOL_SERVICE_ENDPOINT", ] async def check_env(): missing = [k for k in REQUIRED_ENV if not os.getenv(k)] if missing: raise RuntimeError(f"缺少必要环境变量: {missing}") async with aiohttp.ClientSession() as session: try: async with session.get( f"{os.getenv('AGENT_API_BASE')}/health", timeout=aiohttp.ClientTimeout(total=5), ) as resp: if resp.status != 200: raise RuntimeError(f"Agent 服务健康检查失败,状态码: {resp.status}") except asyncio.TimeoutError: raise RuntimeError("Agent 服务健康检查超时")

这一步的价值在于,它把后续所有测试的前提条件固化下来。只要环境探测不过,整套脚本直接退出并给出原因,而不是让你在测试日志里慢慢翻错误。注意,健康检查接口名称、超时时间这些参数要跟你实际部署的服务保持一致,硬编码虽然简单,但最好抽成配置项,方便不同环境切换。

3.2 确定性验证:把“LLM 输出不稳定”变成可断言的结果

确定性验证层要解决的核心问题是:面对同一组输入,Agent 的行为是否符合预期。但 LLM 输出天然有随机性,你不能直接断言“返回结果必须是某句话”,而应该断言“返回结果必须满足某组约束条件”。

我在实际项目里定义了一套校验规则,主要分三类:

  • 结构性约束:返回结果必须是合法 JSON(如果协议要求 JSON)、必填字段不能缺失、字段类型必须正确,比如status必须是字符串、data.tool_calls必须是数组。这类约束用pydantic模型最方便。
  • 语义性约束:某些字段的值必须在合法范围内。比如 Agent 回答里引用的文档编号必须真实存在于知识库中,工具调用参数必须包含上游需要的必要字段。这类约束需要写自定义校验函数,不能靠简单的 JSON Schema 解决。
  • 行为性约束:某些行为不能发生。比如无意义的重复工具调用、偏离用户意图的无效回复、产生不安全的内容。这类约束需要结合实际业务做正则匹配或者关键词过滤。

用代码来展示这个设计会更直观。下面是一个简化的结果校验模块:

from pydantic import BaseModel, Field, ValidationError from typing import List, Optional class ToolCallResult(BaseModel): tool_name: str = Field(..., pattern=r"^[a-zA-Z0-9_]+$") arguments: dict status: str = Field(..., pattern="^(success|error|timeout)$") class AgentOutput(BaseModel): session_id: str content: str = Field(..., min_length=1) tool_calls: Optional[List[ToolCallResult]] = None references: List[str] = [] class DeterministicValidator: def __init__(self, output: dict): self.output = output def validate_structure(self) -> bool: try: parsed = AgentOutput.model_validate(self.output) return True except ValidationError as exc: print(f"结构校验失败: {exc}") return False def validate_semantics(self) -> bool: if self.output.get("references"): # 假设 references 必须在知识库中存在 for ref_id in self.output["references"]: if not self._ref_exists(ref_id): return False return True

这里有一个非常关键的经验:结构校验通过后,仍然不能掉以轻心,因为content字段存在并不代表内容质量达标。我遇到过不少情况,Agent 在长时间会话后出现了“失忆”,明明用户在第一轮交代过偏好设置,后面几轮回答完全忽略了,内容从结构看没问题,但从行为角度看已经偏离需求。所以确定性验证必须配合一些针对“关键约束”的断言。比如你可以在测试输入里加入“在所有回答里必须包含 A 关键词”这类规则,再用脚本去校验输出内容,才能提前暴露这类上下文丢失问题。

3.3 并发校验:模拟真实流量并统计关键指标

并发校验是整个脚本中技术含量最高的一部分,也是最能暴露“Demo 没问题、上线就崩”问题的环节。Agent 系统在并发下的崩溃模式五花八门,最常见的是共享状态污染,比如多个会话共用一个工具注册表、一个全局上下文缓存,导致不同用户的对话内容互相串线;其次是线程安全问题和资源耗尽,比如内存溢出、连接池耗尽。

我在并发模块使用asyncio结合信号量模拟指定并发度,并对每个请求做延迟统计。以下是一个简化版本:

import asyncio import time import statistics import aiohttp async def run_concurrent_validation( request_payloads: list, max_concurrency: int, ): semaphore = asyncio.Semaphore(max_concurrency) results = [] async def single_request(payload): async with semaphore: start = time.perf_counter() try: async with aiohttp.ClientSession() as session: async with session.post( f"{os.getenv('AGENT_API_BASE')}/v1/chat", json=payload, timeout=aiohttp.ClientTimeout(total=60), ) as resp: body = await resp.json() elapsed = time.perf_counter() - start results.append({ "session_id": payload["session_id"], "status": resp.status, "elapsed_ms": elapsed * 1000, "success": resp.status == 200, "output": body, }) except Exception as exc: elapsed = time.perf_counter() - start results.append({ "session_id": payload["session_id"], "status": 0, "elapsed_ms": elapsed * 1000, "success": False, "error": str(exc), }) await asyncio.gather(*[single_request(p) for p in request_payloads]) success_items = [r for r in results if r["success"]] failed_items = [r for r in results if not r["success"]] latencies = [r["elapsed_ms"] for r in success_items] print(f"并发完成: 成功 {len(success_items)},失败 {len(failed_items)}") print(f"P50 延迟: {statistics.median(latencies):.2f}ms") print(f"P95 延迟: {sorted(latencies)[int(len(latencies) * 0.95) - 1]:.2f}ms") print(f"P99 延迟: {sorted(latencies)[int(len(latencies) * 0.99) - 1]:.2f}ms")

这段代码的关键在于使用信号量控制并发度。很多人第一次写并发脚本会直接用asyncio.gather加列表里的所有任务,如果任务量是几十上百个,那等于瞬间打满所有并发,效果等同 DoS,根本测不出真实并发场景下的表现。我建议从低并发逐步往上加,比如 5、10、20、50 依次测试,观察成功率、P95 延迟、错误类型的变化趋势,而不是一开始就猛冲到 100。

同时必须记录失败请求的错误类型。有些失败是超时,有些是 HTTP 5xx,有些是连接重置。不同错误类型对应的排查方向完全不同:超时大多指向 Agent 内部逻辑耗时过长、重试机制失效或者外部依赖响应慢;5xx 可能是指 Agent 服务代码本身存在问题;连接重置往往意味着服务进程崩溃或被 OOM Kill。

3.4 异常注入:让服务在“不配合”的环境里接受检验

异常注入是我做这套脚本后收获最大的一部分。Agent 系统和传统 API 的最大区别是它依赖了大量外部工具和上游服务,而上游服务在生产环境里的表现往往比不上 Demo 环境。如果不在上线前模拟这些异常,真正出问题时你对系统的容错能力毫无把握。

我在系统里主要做了两类异常注入:

第一类是模拟工具调用超时。我在测试 Agent 系统时,故意把工具层的响应延迟拉到很高,观察 Agent 是否能在超时阈值内切换到备选路径,或者给出合理的错误提示。实际测试中发现,不少 Agent 框架在工具超时后只是简单抛错误,并不触发重试或降级策略,最终用户会收到一个莫名其妙的失败回复。

第二类是模拟上游返回脏数据。比如让工具接口返回缺失关键字段的 JSON,或返回不符合预期类型的数据。LLM 在接到这类数据后可能强行“脑补”字段,生成错误的工具调用参数。这类问题非常隐蔽,它不会报错,但业务结果完全错误。用校验脚本把脏数据注入后断言最终输出,能第一时间发现 Agent 对异常数据的处理能力。

下面是一个简化版的异常注入函数:

import random async def inject_tool_slowdown(probability=0.2, delay_range=(5, 15)): """ 在实际测试中,可以通过在 Agent 服务侧的工具调用层动态加延迟。 这里演示一个客户端侧的简易模拟:以一定概率延迟发送请求。 """ async def maybe_wait(): if random.random() < probability: delay = random.uniform(*delay_range) print(f"模拟工具超时,延迟 {delay:.2f}s") await asyncio.sleep(delay) await maybe_wait()

这块要注意一个实操要点:异常注入不应该只在客户端脚本里模拟,最好的方式是在 Agent 服务端留一个“混沌开关”。比如你可以在服务里配置一个环境变量ENABLE_FAULT_INJECTION=true,然后在工具调用层判断是否注入延迟或返回脏数据。这样测试更真实,能覆盖到服务内部真正处理异常的逻辑。如果服务端做不到,退而求其次就在客户端模拟,但效果会打折。

3.5 Token 成本和上下文长度校验

很多 Agent 项目上线翻车不是因为功能坏了,而是因为成本爆了。LLM 调用费用和 Token 消耗直接相关,尤其是长会话场景,上下文越长,每次调用的 Token 数越大。我在校验脚本里专门加了一个成本监控模块,用一组代表性的长会话输入跑完整流程,统计单次任务的平均 Token 消耗量和总消耗量。

这里有个极具参考价值的细节:我发现一些 Agent 在长期运行后存在上下文无限膨胀的问题。设计者本意是保留完整对话历史,但几百轮之后,每次请求的 Token 数呈现线性甚至指数增长,既不经济,也容易突破模型的上下文窗口。校验脚本在连续会话测试中直接记录每一轮的 Prompt Token 数量,一旦发现增长速度异常就报警。

def estimate_token_count(text: str) -> int: """ 简单估算 Token 数:中文按 1.5 字符/Token,英文按 4 字符/Token 近似。 生产环境更推荐使用官方 Tokenizer。 """ chinese_chars = sum(1 for c in text if "\u4e00" <= c <= "\u9fff") other_chars = len(text) - chinese_chars return int(chinese_chars * 1.5 + other_chars / 4)

Token 估算值不一定精确,但趋势监控完全够用。我更建议你在服务端开启 Token 计费日志,把每次请求的 usage 信息打到日志系统里,然后校验脚本通过日志接口拉取数据做分析,这样获得的数据更准确。校验脚本做这件事的价值在于:它能通过连续多轮测试告诉你“这个 Agent 在长会话下会不会资源失控”,而单纯的功能测试往往忽略了这一点。

3.6 结果报告与持续集成

所有校验项跑完后,脚本会汇总生成一份 JSON 报告,包含每个测试项的通过状态、失败详情、关键指标。这份报告不仅是为了人工查看,更重要的是可以接入 CI/CD 做质量门禁。

我在项目里的做法是:把校验脚本打包成 Docker 镜像,在合并请求的 Pipeline 中跑一个快速子集(环境探测、确定性验证、少量并发),在发布前的 Pipeline 里跑全量子集(包括异常注入、长会话成本测试)。一旦任何一环节失败,发布流程自动阻断。这样就让“上线前校验”变成流程的一部分,而不是靠某个工程师想起来才去跑一次。

# 伪流水线配置示例,实际使用可根据你的 CI 平台调整 stages: - validate - deploy validate: stage: validate image: python:3.11-slim script: - pip install -r requirements.txt - python toolchain/validate.py --profile full only: - main - tags

刚开始接入 CI 的时候,团队里有人担心这套脚本会增加发布耗时,实际上跑完整校验一般也就 3 到 5 分钟,相比线上翻车后花几个小时排查问题,这个成本完全可以接受。

4. 实测中遇到的经典问题与排查实录

4.1 并发导致的全局状态污染

在一次针对工单自动分类 Agent 的测试里,我设置 50 个并发请求,每个请求指定不同的session_id,结果发现多个会话的回复内容互相串了。排查后发现,问题不在 LLM 层,而在 Agent 框架里一个“工具调用历史记录”用了全局数组,没有按会话隔离。这个 Bug 在 Demo 环境完全不会暴露,因为单用户单请求时,全局变量天然安全。但并发一到,数据互相覆盖,用户看到的就是“A 用户的工单描述出现在了 B 用户的处理结果里”。

这类问题的排查方法也很简单:在并发测试的请求 payload 里加入不同标记,比如不同的用户 ID、不同的业务关键词,然后在响应里做关键词匹配,确认每个响应都严格对应自己的输入。如果出现串号,立刻把并发降为 1,再复现一次,问题还会不会发生。如果不会,基本就能断定是共享状态问题。

4.2 偶发失败是 LLM 输出格式漂移

另一个高频问题看起来非常诡异:同一个测试用例,跑一百次能过九十五次,剩下的五次失败原因完全不在代码逻辑内。具体表现是 LLM 偶尔会在tool_calls字段里返回一个不符合预期结构的对象,比如把数组写成了对象,或者字段名从tool_name变成了toolName。这类“偶发失败”最容易让研发团队陷入负面情绪,因为你不管怎么查代码都找不出问题。

我的解决思路是不要只盯着代码逻辑,而是把 LLM 输出原样记录下来,专门做“格式漂移率”统计。让模型连续跑 200 次,统计格式不合格的次数比例。如果没有快速恢复机制,哪怕 1% 的漂移率在生产环境也是不可接受的,因为生产环境一天可能有几十万次调用,1% 就是几千次失败。对此可以采用两种对策:第一种是在调用层增加格式修正提示,遇到格式不合格就让模型重新生成一次;第二种是在 Agent 服务端做输出规范化,用 JSON 修复工具强行处理。校验脚本真正的作用是告诉你漂移率有多高、问题集中在哪些字段,而不是替你修复它。

4.3 工具返回大 Object 把内存打爆

在一个检索类 Agent 项目中,我测试的是“工具返回结果解析”环节。工具的原始返回是一个可能包含上百万字符的 JSON 文档,直接塞给 LLM 当上下文的一部分。Demo 阶段这个文档比较小,没出问题;生产环境一旦文档变大,要么 Token 超限,要么 Agent 服务内存被打爆。

我在校验脚本里专门加了一个“超大返回测试”,用一个明显超出正常期望的返回数据去请求 Agent,看它是优雅截断还是直接崩溃。实测结果是服务进程直接 OOM,日志只有一行“Killed”。这个问题的解法是在工具调用层和 LLM 调用层之间加一个结果摘要模块,对超长返回做截断、压缩和关键信息抽取。校验脚本的价值在于明确告诉你“哪些环节没有防御机制”,从而让团队有针对性补齐。

4.4 沙盒环境和真实服务的差异

不少 Agent 框架在本地通过沙盒执行工具调用,比如动态生成的 Python 代码在容器里跑。Demo 阶段沙盒启动很快,工具执行顺利;生产环境沙盒需要冷启动,加上网络策略限制,工具调用失败率和超时率都会上升。

我的校验脚本在环境探测和异常注入模块里分别加了沙盒相关检查。环境探测会验证沙盒镜像是否可用、能否正常拉取依赖;异常注入会故意让沙盒里产生运行时报错,看 Agent 能不能拿到错误信息并返回给用户。实测发现有一些 Agent 在沙盒报错后直接进入死循环,因为它把“沙盒运行失败”当作可重试错误,但每次重试都是同一个失败原因,白白浪费时间和 Token。这类问题从功能测试角度很难发现,但通过脚本自动化跑一遍就能暴露。

4.5 成本失控比功能错误更致命

有一次跑长会话测试,我连续发起了 80 轮对话,脚本统计到第 60 轮时单次请求的 Token 已经翻了 5 倍。功能上一切正常,回答依然正确,但成本已经涨到无法接受。如果只做功能校验,这个问题绝不可能被发现;因为没有人会在 Demo 环境手动跑 80 轮去观察 Token 变化趋势。

我后来给校验脚本加了一个“上下文增长门控”规则:如果连续 20 轮会话内,Token 消耗量增长超过固定阈值,就判定为风险,提示研发团队检查上下文管理策略。实际应用中,这个规则至少帮我们提前发现过两处因为历史消息存储策略不当导致的成本隐患。

5. 这套脚本的扩展性与其他落地建议

5.1 与 Golden Set 机制结合

除了上面提到的校验维度,我还推荐把“Golden Set”机制引入到确定性验证层中。所谓 Golden Set,就是从历史真实流量中选取一批代表性请求,连同它们的“期望行为”一起固化下来,作为回归测试的基准集。跟普通固定用例的区别是,Golden Set 的来源是真实用户请求,覆盖了更多长尾场景。

我在项目里维护了一个golden_set.json文件,每条记录包含input_session、expected_keywords、expected_tool_patterns、forbidden_keywords等字段。校验脚本每次发布前会全量跑一遍 Golden Set,作为对 Agent 行为漂移的基线检查。因为这个集合会随业务演进持续更新,相当于给 Agent 行为上了一道持续进化的安全网。

具体文件结构类似:

[ { "name": "工单分类-含附件关键词", "messages": [ {"role": "user", "content": "帮我处理一个紧急工单,内容是登录页面报错,影响用户下单"} ], "expected_tool_calls": ["classify_ticket", "check_severity"], "expected_status": "resolved", "forbidden_keywords": ["无法处理"] } ]

Golden Set 的维护成本不算低,但对长期迭代的 Agent 项目来说非常值得。它解决了“每次改动不知道会不会回归”的焦虑,让团队敢于持续优化 prompt 和框架版本。

5.2 校验频率和场景选择

不是每次改动都需要跑全量校验,我一般把校验脚本分成三个 profile:

  • smoke:环境探测 + 少量确定性验证,适合开发环境每次提交代码后快速检查,耗时控制在 1 分钟以内。
  • regression:全量确定性验证 + 中等并发 + Token 成本检查,适合每次更新框架版本、修改 prompt 后运行,耗时大概 3 到 5 分钟。
  • full:全量确定性验证 + 高并发压测 + 异常注入 + 长会话成本测试 + Golden Set 回归,适合发布前和灰度启动后运行。

这种分级策略既保证了质量门禁,又不会让研发效率降低太多。实际使用中,smoke 和 regression 跑得最频繁,full 跑得少但价值最高,基本每次发布都能捞到一两个真实隐患。

5.3 运维侧配合:日志和可视化

校验脚本只负责发现问题,真正快速定位问题还需要服务端有完善的日志支撑。我在 Agent 服务里强制要求每次调用都打印request_id、session_id、input_token_count、output_token_count、tool_call_count、elapsed_ms这些字段,并且把日志汇聚到统一的日志平台。校验脚本在发现异常时会把session_id输出,运维同学依据它直接在前端链路追踪里查完整调用链。

如果没有这种日志基建,校验脚本发现的问题往往只能定位到“某个环节失败”,很难精准找到“哪一调用哪一参数导致失败”。这是我踩了几次坑以后总结出的一条硬经验:自动校验和链路追踪是配套的,少了任何一个,排查效率都会大打折扣。

6. 踩坑集锦:给后来者的几条实在建议

6.1 别让校验脚本本身成为不稳定因素

校验脚本要长期稳定运行,最怕的是脚本自己先崩。我在写脚本时踩过两个坑:一是并发请求数设得过高,导致被测试服务把脚本所在机器的 IP 临时封禁,后续请求全部失败;二是请求超时时间设得太长,一旦服务端假死,脚本会卡住不动,而不是快速超时并报错。

针对这两个问题,我的建议是:在并发模块里加入动态退避机制,一旦发现连续失败达到阈值,自动降低并发度,避免脚本成为压垮服务的最后一根稻草;请求超时时间根据不同校验场景设置合理值,比如普通问答设 30 秒,复杂任务设 120 秒,而不是全局统一 60 秒。

6.2 数据隔离和测试标记

校验脚本会在被测系统里创建大量测试会话,如果这些会话没有特殊标记,很容易污染线上数据。我统一约定所有测试session_id以_test_开头,并在创建测试数据时加source=validation字段。线上监控和数据分析环节可以根据这些标记排除测试流量。

另外,如果 Agent 系统会真实调用外部工具写数据,比如发送邮件、创建订单,校验脚本必须做一层“测试模式”拦截。比如在测试模式下,邮件发送接口改成本地日志输出,订单创建接口改成测试专用的模拟服务。否则每跑一次校验,外部系统就多出一堆脏数据,这个副作用可比 Bug 本身还麻烦。

6.3 持续维护校验用例

校验脚本最怕的不是写不出来,而是写出来后没人维护。随着 Agent 功能迭代,新的工具、新的行为模式会不断出现,固定用例如果跟不上变化,脚本就会产生大量误报。我的做法是每周固定留出一个小时,跟研发和产品一起 review 新增功能点,更新 Golden Set 和确定性校验规则。这套机制虽然简单,但能保证校验脚本始终贴近当前系统的真实行为。

写在最后的体会

做这套自动校验脚本的过程中,我最大的感受是:Agent 开发最难的从来不是把 Demo 跑通,而是让它在真实环境里持续正确地工作。Demo 跑通只说明最小路径可行,生产环境要求的是全路径可靠。校验脚本不可能消除所有线上问题,但它能把最常见、影响最大的问题提前到发布前暴露,让团队在安静的环境里修复,而不是在线上告警的噪音中救火。

从我个人的实践经验来看,这套脚本对中小型 Agent 项目带来的价值尤其明显。大厂可能已经有完整的全链路测试平台,但普通团队往往只能靠几个人、几台机器硬扛。用 Python 写一套轻量校验脚本,成本几千行代码,换来的是每次发布前的一点底气。如果你也在做 Agent 项目,强烈建议尽早把校验脚本纳入开发流程。等到线上翻车再补,代价往往不止是几行脚本的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询