自从把一个大模型接口和几个工具脚本拼到一起,我得到了一个能帮我查天气、记日程、搜资料的自用 Agent。最初几天很新鲜,什么任务都丢给它,结果某次它把日程提醒里的时间记错了,差点误事。那一刻我意识到,自用 Agent 既然要长期信任,就不能停留在“大部分时候能跑”的层面。
自用 Agent 的架构并不复杂:底层是 LLM 调用,外层挂了工具注册、记忆存储、定时调度。可恰恰因为每个环节都可能出问题,才需要一套覆盖“对话、工具、记忆、调度、安全”的全面功能测试方案。这不是为了给项目凑指标,而是为了在日常使用中敢把真正重要的事情交给它。
我给这次全面测试定下了四个目标:功能正确性、异常恢复能力、成本可控性和数据安全性。功能正确性指任务结果是否符合预期;异常恢复能力指遇到超时、无效参数时能否优雅失败;成本可控性指不会因为循环调用导致账单失控;数据安全性指记忆和日志中不出现隐私泄露或越权访问。这几点在自用场景里很容易被忽略。比如成本控制,个人开发者通常会选按量付费的大模型 API,一旦 Agent 陷入自我对话的循环,一分钟内就能烧掉不少 token。所以我把测试重点锁定为长期稳定运行、边界条件和可观测性,而不是只测几个 happy path。
1. 为什么自用 Agent 也需要一套完整测试方案
1.1 自用 Agent 并不是“能跑就行”
很多个人项目的通病是:功能跑通一次就算完成。对普通脚本来说也许说得过去,但 Agent 不一样,它具备状态、调用外部工具、维护记忆,任何一环出问题都可能让结果悄悄变错。我那个记错时间的例子,就是典型的“能跑但不可靠”。
从测试角度看,Agent 的不确定性来自两层。第一层是模型本身,同一个 prompt 在不同温度、不同版本下输出会有差异;第二层是工程链路,工具返回异常、记忆检索排序、调度时间冲突都可能让行为偏离预期。这两层叠加之后,人工试几次根本说明不了问题,必须用可重复的用例去逼近真实风险。
所以我把这次“全面功能测试”的目标拆成了五类:对话语义是否准确、工具调用参数是否正确、记忆是否保持一致、定时和事件触发是否准时、以及异常和成本是否可控。每一类都不是单独测一遍,而是和真实使用场景绑定,做成固定用例和回归基线。
1.2 我理解的“全面”到底是什么
我见过一些人做测试,把所有 prompt 丢给模型看回复是否“像那么回事”,然后统计一个准确率。这种做法对自用 Agent 来说远远不够。工具调用不是聊天,参数填错哪怕回复说得漂亮也会出事故;记忆和调度更依赖时序,单轮问答根本暴露不了问题。
因此我在测试方案里做了分层:对话层关注意图识别与上下文连续,工具层关注参数 schema 与调用序列,记忆层关注数据一致性与过期,调度层关注时间表达与并发冲突。最后再加上一层全局的“可观测性检查”,也就是让每次运行都能留下结构化日志,万一出错可以回溯。
这套分层思路也决定了后面所有测试用例的写法:除了验证最终回复,还要验证中间过程。比如用户说“把明天下午的会改到后天上午”,我不能只看 Agent 是否回复“已修改”,还要检查它是否准确调用了日历工具的“修改事件”函数,并且参数里的时间字段确实对应后天上午。中间过程验证,才是功能测试和聊天评测的根本区别。
2. 测试环境怎么搭,才不会把真实数据搞乱
2.1 隔离运行环境与 Mock 工具
自用 Agent 里挂着真实邮箱、日历、文件系统操作,直接在原环境里测试风险很大。我最终采用了三层隔离:模型接口层用一个固定 prompt 和本地假响应服务器;工具层全部注册成可切换的 Mock 实现;数据层用单独的 SQLite 文件和一个临时目录放文件。别小看这个临时目录,它让我敢测试“批量删除文件”这种危险操作,就算用例写错也不会波及任何真实数据。
隔离的代价是会带来一定“仿真度”损失。比如 Mock API 永远不会有网络抖动,固定模型响应也掩盖了真实模型的随机性。我的处理方式是:把测试环境分为两档。第一档是全 Mock 环境,适合日常开发和回归;第二档是半 Mock 环境,模型接口用真实 API,但工具和数据仍指向测试实例,适合上线前的冒烟验证。两档之间用环境变量切换,配置成本很低。
如果你也想给自用 Agent 做测试,我强烈建议先把工具层抽象出来。不要直接在业务代码里调用 requests 或 subprocess,而是定义统一的工具接口,再根据环境加载不同实现。这样不仅能测试,以后扩展新工具也会方便很多。
2.2 用 pytest 组织测试用例,配合快照断言
命令行工具可以一条条人工验证,但 Agent 是多轮交互和工具调用的组合,手工测试无法覆盖组合爆炸。我选择了 pytest 作为主测试框架,把每个场景写成用例。对文本型输出,我会先抓取关键字段再断言;对结构化工具调用,则直接断言参数 JSON。
下面是我用来验证“日程添加”的一个简化示例:
def test_add_schedule_uses_correct_params(mock_agent): result = mock_agent.run("帮我后天下午三点添加日程:和A同学讨论测试方案") call = mock_agent.last_tool_call("add_calendar_event") assert call is not None assert call["params"]["title"] == "和A同学讨论测试方案" assert call["params"]["time"] == "2025-06-02 15:00"这种断言方式,避免了只检查“Agent 最后回了一句话”的弱验证。除了参数校验,我还大量使用了快照测试。固定输入 prompt,把工具调用参数和最终回复存成 JSON 指纹,后续改动只要能匹配快照,就说明没有破坏原有行为。快照测试最适合抓“回归”,比如模型升级后某个工具参数突然多了一个默认值,快照一眼就能看出来。
2.3 测试数据和 Prompt 基线
我整理了一份大约 120 条任务的测试集,包含日常问答、多轮上下文追问、工具参数边界、指令冲突、敏感信息询问等。每条任务都预先定义了期望行为,包括是否应该调用工具、期望参数值,以及禁止返回的内容。这里要重点提醒一句:测试集必须从真实聊天记录里抽样,不要自己凭空编一堆“标准问法”。真实使用里有大量口语化表达和省略主语,比如“后天那个事别忘了”这种话,Agent 很容易误解。
Prompt 基线同样重要。同一个 Agent 在不同 system prompt 版本下行为差异极大。我会把当前生效的 system prompt 保存为带版本号的文件,测试报告里会输出版本号,避免“现象消失了但不知道改了啥”的尴尬。对于动态生成的 prompt,我也会在测试夹具里固定伪随机种子,保证文本生成过程可复现。
3. 分层来看,Agent 的哪些功能最值得测
3.1 对话层:意图识别与多轮上下文连贯性
最基础也最容易低估的是对话层。自用 Agent 需要识别用户意图,并区分“闲聊”与“希望调用工具”。我遇到过把“把明天的行程发给我”理解成“发邮件给明天的行程”的情况,这类错误单看一句回复很难发现。
对话层测试我按三档设计:单轮正确性、多轮上下文保持、长对话记忆衰减。多轮用例会刻意在第 3 轮或第 5 轮引用前面提到的实体,比如“还是刚才那个人,时间改成周六”,检验 Agent 是否能正确替换旧信息。长对话测试则会插入 40 轮无关闲聊,验证关键信息是否被覆盖。
测试中还发现一个规律:上下文窗口越大,模型越容易忽略最开始的约束。所以测试用例里要反复验证“首轮指令约束在后期是否仍然生效”。举个例子,第一轮声明“以后所有提醒提前 30 分钟”,第五轮让 Agent 新增一个提醒,结果它只按默认的 10 分钟处理,这就算一条失败用例。这种问题在纯人工体验中很难抓,因为只有把输入输出对比放一起看,才看得出约束失效。
3.2 工具层:参数校验、错误传播与多步调用
工具调用是自用 Agent 扩展性的核心,也最容易出问题。我重点测了五类场景:正常参数生成、缺失参数补全、非法参数拒绝、工具返回错误后的处理、多工具组合调用。
参数补全尤其考验设计。如果用户只说“帮我提醒我明天开会”,Agent 需要填出时间、标题、地点等字段。合理的策略是向用户确认缺失字段,而不是拍脑袋猜一个。测试用例会覆盖“全部参数齐全”“缺时间”“时间模糊”“地点不存在”等分支,观察 Agent 是回问还是直接乱填。
错误传播也很重要。当工具抛出异常后,Agent 是把原始堆栈返回给用户,还是提炼成一句可理解的提示?我见过某次测试里,Agent 直接把工具内部路径和异常栈暴露了出来,虽然功能上“完成”了,但信息泄露是很危险的。后来我在 system prompt 里明确要求:所有工具异常必须归类输出,只给出用户可理解的原因和可能的修复建议。
多步调用测试通常用综合场景来压。比如“查一下某地天气,顺便提醒我明天带伞”,正确流程是先调天气工具,拿到结果后把“带伞”写入提醒。我会用记录所有调用序列的日志来断言顺序,并检查中间结果在第二步是否被正确使用。多步调用最容易出现“中间结果丢失”或者“错误地复用了上一次调用的返回值”,必须在用例里显式验证。
3.3 记忆层:一致性、过期与跨会话持久化
自用 Agent 的记忆层我采用了 SQLite 加向量检索的方案,由 system prompt 注入最近相关记忆。功能测试需要验证记忆写入的准确性、读取时的相关性排序,以及删除和过期机制是否生效。
记忆写入容易踩的坑是“过度泛化”。测试时我发现,当我说“我喜欢看科幻电影”,Agent 会在记忆表里写入“用户喜欢电影”。这虽然不算错,但信息精度不足,会影响后续推荐。因此记忆测试不仅要验证存储结构,还要校对存入的具体字段,确保“电影类型”和“偏好强度”都被完整记录。
过期机制我设计为:记忆带时间戳,超过 30 天未命中的记录会降权。测试用例会构造一条旧记忆,观察它是否在回答中被忽略。跨会话持久化测试则模拟“今天记录一个偏好,明天新开会话仍能命中”。这一类测试最依赖时间,但可以用固定时间偏移注入来模拟,不必等真实时间流逝。
3.4 调度层:定时任务与事件触发的边界
自用 Agent 如果支持定时提醒或周期性任务,调度层就要单独测。这里最大的问题是时间表达。测试用例会把“每天上午九点”“周末下午”“每隔两小时”这些自然语言输入转成 cron 表达式,再验证触发时间点是否准确。
事件触发与手动调度的状态冲突也要重点覆盖。比如一个定时任务正在执行,同时用户手动发起同一类任务,两个请求会不会抢占同一个工具实例?我遇到过查询任务在并发执行时把中间结果写到同一个全局变量,导致两个请求的结果互相覆盖。这是非常隐蔽的状态一致性 bug,单次调用永远复现不了。
调度层测试不能用真实时间硬等,我使用一个虚拟时钟:把“现在”设置为指定时刻,让调度器按虚拟时间推进,校验任务是否在期待时间被触发。代码层面,只需要在初始化时注入一个 clock 对象即可,改动成本很低,收益却很大。如果你的 Agent 有“瞎跑”“重复执行”的毛病,多半就是调度层没做幂等控制,这类问题也能用虚拟时钟复现。
4. 实测中发现的问题与处理思路
| 问题 | 现象 | 根因 | 修复方案 |
|---|---|---|---|
| 参数幻觉 | 模型填入不存在的年份限定 | 上下文模糊信息被当成精确参数 | 工具描述强调未知字段不得猜测,执行前做 schema 校验 |
| 记忆污染 | 旧信息叠加到新会话 | 记忆检索没考虑时间衰减 | 增加 freshness 时间惩罚系数 |
| 超时悬挂 | 第三方 API 延迟导致任务卡死 | 缺少超时机制和看门狗 | 工具级超时 + 总执行超时拦截 |
| 成本失控 | 异常循环调用消耗大量 token | 缺少调用次数和 token 上限 | 增加熔断开关和调用链日志 |
| 并发覆盖 | 两个任务互相覆盖临时状态 | 可变状态存进全局变量 | 每个任务独立 context,共享资源加锁 |
4.1 参数幻觉:模型编造了不存在的参数值
第一个严重问题出现在工具调用参数生成。测试用例要求调用图书馆查询工具,传入参数为书名关键词,但 Agent 在用户没说年份的情况下,主动填入了“2023年”,并把限定条件写进查询。这会导致查询结果被无谓地过滤掉,用户明明想看所有相关书籍,结果只返回了某一年份的。
根因是 user prompt 里恰好出现了“去年的书”这种上下文,模型把模糊信息当成了精确字段。我在工具描述里加入了“不存在的字段不要猜测,以用户原话为准”,同时增加了解析层校验:工具执行前对所有参数做 schema 校验,缺失或非法参数直接进入澄清流程,不进入工具调用。
澄清流程本身也要测试。不能让 Agent 进入“反复追问同一个问题”的死循环,所以我在用例里规定最多澄清两次,如果两次之后参数仍然不完整,就默认填充一个安全值并在回复里显式提醒用户。安全值根据不同工具定义,比如时间缺省会填“当天 18:00”并在后续消息中说明。
4.2 记忆污染:旧信息被错误叠加到新会话
第二个问题有关记忆读取。早上我问过“今天下午有雨”,下午新开会话问“我需要带伞吗”,Agent 正确调用了天气工具,但回答时却把早上已过期的一句话混进了结论,造成输出前后矛盾。排查后发现,记忆检索的相关性排序没有考虑时间衰减,导致旧记忆权重过高。
修复思路是给记忆记录增加 freshness 字段,在检索打分时乘以时间惩罚系数。比如 30 分钟前的内容权重是 1.0,超过 12 小时降为 0.3,超过 3 天降为 0.1。同时,当一条记忆被用户明确否定后,要立即标记为“不可用”,从后续检索中排除。
自用场景下尤其要重视“否定记忆”。用户说“不是”“不要”之后,相关工作流应立刻停止引用旧信息。我在 system prompt 里加了一条规则:如果当前提问中有明确的否定词,记忆检索模块必须在结果集里过滤掉方向相反的旧记录。这条规则配合打分降权,基本解决了记忆污染问题。
4.3 超时悬挂:第三方 API 迟迟不返回
工具调用依赖外部 API 时,经常遇到请求超时。我把某个天气 API 的超时测试场景做成:服务端延迟 120 秒,Agent 默认等待,导致整个任务卡住,既不继续也不退出。用户侧看起来就是“AI 已死机”。
处理方案有两条。一是在工具类里统一设置超时和重试上限,比如默认 10 秒超时,最多重试 2 次,超时后抛出业务异常。二是给整个 Agent 增加总执行超时看门狗,超过配置的时间自动中断,并返回“当前任务太复杂,请简化后重试”的提示。
实测下来,看门狗生效前必须先防止子任务自行阻塞主线程。我是把所有工具调用放到线程池的独立 worker 中,主线程等待结果时设置整体 deadline。如果一个工具线程卡死,至少不会拖垮主流程。注意这里不要用裸线程操作共享状态,否则会引入新的并发问题。
4.4 成本失控:一次错误循环消耗大量 token
测试某次遇到工具返回大量异常文本,Agent 没有停止,而是反复调用归纳工具去“消化”异常,形成“结果 → 异常 → 归纳 → 再调用”的死循环。等到我手动终止时,一个测试会话已经消耗了正常情况 20 倍以上的 token。
为了应对,我在调度层加了两个熔断开关:单任务最大工具调用次数默认 10 次,单任务累计 token 上限默认 20000。达到阈值立即停止,并记录完整的调用链到日志。同时把每次测试的 token 消耗统计输出到报告里,方便对比不同 prompt 版本的费用差异。
这里有个实践细节:熔断触发后的提示要区分“用户请求无法完成”和“系统保护性终止”。前者是 Agent 需要再次追问,后者是直接告诉用户已经中断并附上部分结果。测试用例里不仅要验证开关能触发,还要验证开关触发后的用户提示是清晰且不恐慌的。
4.5 并发状态覆盖:共享变量的幽灵 Bug
最开始我并没有把并发当回事,直到一次定时任务和手动任务同时触发,两个任务操作同一个“临时文件路径”变量,后写入的任务覆盖了先写入的任务,导致两个流程都读到错误内容。这种 bug 只在并发下出现,单次测试完全不会被暴露。
修复方式很简单:把所有可变状态收进每个任务独立的 context 对象,工具函数不再访问全局变量。对于必须共享的资源,比如日志文件,使用线程安全的写入锁。测试侧用多线程夹具同时发起多个任务,断言每个任务拿到的 context 是独立的。
这类并发问题在个人项目里很容易被忽略,因为乍看 Agent 是“一句一问”,似乎天然串行。但一旦加上定时任务、异步通知、多开对话窗口,共享状态就会变成定时炸弹。我的建议是:从一开始就规定所有临时状态必须挂在 context 上,不要用模块级变量。
5. 把测试沉淀成一套可重复使用的回归体系
5.1 从手工用例到自动化的三层断言
我不希望这些测试只做一次,所以把所有用例都固化成了自动化脚本,并分成三层:单元测试负责纯函数和工具逻辑,集成测试负责记忆库与调度器,端到端测试负责完整的用户输入到最终回复。端到端测试使用固定模型响应或本地 mock 响应,把不确定性彻底锁死。
层与层之间的数据流也要测。比如集成测试会验证:记忆写入后能被检索到,调度器触发的任务能正确进入工具调用链。每一层失败时,报告要能定位到是哪一层的问题,而不是直接抛出一堆日志。我给每个测试用例加了 metadata,标明对应需求编号和功能模块。
这套三层策略看起来普通,但对自用 Agent 十分有效。很多个人项目只有“端到端”测试,一旦失败不知道问题出在模型还是工具还是记忆。拆开后,我常常看到单元测试全绿,但端到端失败,问题马上聚焦到 prompt 或参数传递环节,排查时间大幅缩短。
5.2 录制回放与快照比较
个人项目最大的难点是大模型输出不稳定。昨天测试通过,今天同样的 prompt 可能就不通过。我采用了两套缓解手段:一是录制回放,保存真实运行中的工具调用记录和响应,测试时重放这些记录,保证行为一致;二是对长文本输出做“关键语义指纹”,比如提取数字、日期、实体,比对指纹而非全文。
快照比较需要定期更新,不然模型升级后会让测试大面积飘红。我的做法是给快照加版本号,并在更新时人工审查一次差异。重点看是否新增了非法输出、参数是否变得不符合预期,而不是追求自动化完成所有审批。
录制回放也有局限:它只能验证“行为没有变化”,不能验证“行为是否更好”。所以我会在关键测试集上保留真实模型冒烟测试,每周跑一次,看语义指纹是否符合预期。真实验证和回归验证穿插使用,算是一个比较稳妥的组合。
5.3 可观测性监控:成功率和成本都要看
测试只是阶段性的,日常运行更需要监控。我给 Agent 增加了结构化日志,记录了每次请求的输入长度、输出长度、token 消耗、工具调用列表、耗时和错误码。每周汇总成一份趋势报告,重点看成功率、平均耗时和工具错误分布。
这些指标不一定都需要实时告警,但至少要在出现异常时能回溯。例如某次模型的工具输出格式悄悄变化,导致解析失败率从 2% 升到 15%,如果只看聊天回复很难发现,但我从工具调用失败率曲线上一眼看到异常,并快速定位到了模型更新带来的格式变化。
另一个值得监控的指标是“工具调用次数分布”。正常情况下一个任务平均调用 2-3 次工具,如果某个时段平均次数突然暴涨,很可能出现了循环调用或 prompt 设计问题。这个指标比准确率更能暴露 Agent 的“内耗”,强烈建议加到日报里。
6. 全面测试之后,我的经验和后续方向
6.1 给同样维护自用 Agent 的人几点建议
全面测试做下来,最大的体会是:不要追求一次覆盖所有,而是按使用频率和风险优先级排序。对话正确性、工具参数、异常恢复这三件事最值得先测;安全与成本检查则要紧盯实际运行日志,别等项目出问题才去找原因。
另外,测试用例要尽量贴近真实使用习惯,而不是写一堆理想化的 prompt。我最初写了大量“很标准”的提问,结果真实使用中的口语化表达、省略主语、指代不清等全部没有覆盖。后来我把聊天记录里真实出现的问题批次抽样,转化成回归用例,效果立刻好了很多。
还有一点:mock 数据要定期更新。工具返回格式变了,mock 还停在旧格式,测试结果就会失真。我每次更新真实工具接口时,都会同步跑一遍“mock 与真实 schema 一致性”的检查,确保测试环境没有偏离生产环境。
6.2 下一阶段:引入对抗样本与多模型对比
这次全面测试让我确信,自用 Agent 也可以被当成一个正式项目来维护。下一阶段我打算引入对抗样本测试,专门用刁钻的 prompt 试探工具越权、系统指令注入和隐私泄露。再搞一个多模型对比基线,同一批测试用例在不同模型上跑,选择成本与效果最均衡的方案。
代码和用例会继续维护在本地,与生产环境隔离。整套测试框架现在还比较轻量,但已经足够让我在日常迭代中睡个安稳觉。如果你也维护了一个自用 Agent,建议从这次思路里的“三层隔离 + 分层用例 + 监控指标”开始,哪怕只先跑通十几个用例,也会比纯靠感觉靠谱得多。