从Prompt工程到系统化评测:构建生产级Agent的工程实践
1. 从“炼丹”到“工程”:Agent评测的范式转移
最近和几个做AI应用的朋友聊天,发现一个挺有意思的现象。大家聊起Agent(智能体)开发,话题总是迅速滑向“Prompt工程”——怎么设计系统提示词、怎么用思维链(Chain-of-Thought)、怎么让模型更好地调用工具。仿佛Agent的好坏,全系于Prompt这一根弦上。这让我想起几年前做传统软件测试,如果有人说“我们软件质量全靠测试用例写得好”,你肯定会觉得哪里不对。测试用例是方法,是工具,但它背后需要一套完整的质量体系、评测标准和工程实践来支撑。
“不是靠Prompt”这个标题,恰恰点破了当前Agent领域一个普遍的认知偏差。我们花了太多精力在“前端”的交互设计上,却忽视了“后端”的工程化评测。一个能说会道的Agent,未必是一个可靠、高效、可维护的智能体。真正的挑战在于:当你的Agent不再是一个简单的聊天机器人,而是一个需要自主理解目标、规划步骤、调用工具、处理异常并完成复杂任务的“数字员工”时,你如何系统地评估它的能力、稳定性和业务价值?
我最近深度参与了一个大规模Agent重构与评测项目,核心代码库经历了超过31万行的重构。这不是一次简单的代码优化,而是一次从评测理念到工程实践的彻底革新。整个过程让我深刻体会到,脱离系统化评测的Agent开发,就像在黑暗中造车,你可能造出了一辆外形炫酷的跑车,但根本不知道它的发动机是否可靠、刹车是否灵敏、能否安全抵达目的地。今天,我就结合这次实战,抛开那些玄乎的Prompt技巧,聊聊Agent评测里那些实实在在的“工程活”。
2. 重构的起点:为什么31万行代码必须推倒重来?
项目初期,我们有一个已经运行了半年多的Agent系统,接入了多个大模型,能够处理数十种任务类型,从简单的信息查询到需要多步工具调用的流程自动化。表面上看,它运行得“不错”,能回答大部分问题。但当我们试图将它部署到对稳定性和准确性要求极高的生产环境时,问题接踵而至。
2.1 “不错”背后的混乱:旧系统的四大顽疾
第一,评测手段极度匮乏且主观。当时的“评测”基本等于人工抽查。产品经理或工程师随机输入几个问题,看看回答“像不像那么回事”。没有量化指标,没有回归测试,A工程师觉得好的回答,B工程师可能认为不及格。这种主观性导致迭代方向模糊,今天优化Prompt让任务A成功率提升,明天可能无意中让任务B彻底失效。
第二,代码结构耦合严重,无法隔离评测。Agent的核心逻辑、工具调用、状态管理、Prompt模板全部搅在一起。你想单独测试“在给定正确工具参数的情况下,Agent能否正确调用工具”?对不起,你必须启动整个应用,模拟完整对话。这种高耦合度使得编写低成本、高并发的单元测试或集成测试几乎不可能。
第三,缺乏可复现的评测环境。大模型的输出具有随机性(即使温度设为0,也可能因服务端变化而产生差异),外部工具API可能不稳定,网络会有延迟。我们的旧系统没有对这些变量进行控制和隔离,导致每次测试结果波动巨大,无法判断性能变化是源于我们的代码改进,还是外部因素的噪音。
第四,没有基准(Benchmark)和持续集成(CI)。我们不知道系统的“基线”性能是多少,因此任何改动都无法评估是进步还是退步。代码提交后,没有自动化的测试流水线来保障核心能力不被破坏。
这四大问题让我们的Agent项目陷入了瓶颈。每次发布新版本都提心吊胆,所谓的“优化”更像是一种赌博。于是,我们下定决心,启动了一次以“可评测性”为核心目标的重构。31万行的改动,首要目的不是增加新功能,而是为这个复杂的智能系统建造一个精密、可靠的“质检中心”。
2.2 重构的核心原则:关注点分离与接口标准化
重构不是蛮干。我们确立了几个核心原则:
- 将Agent“原子化”:把庞大的Agent拆解成独立的、可测试的组件。例如:意图识别模块、规划器模块、工具执行模块、状态管理模块、响应生成模块。
- 定义清晰的接口:每个模块之间通过明确定义的API(函数接口或消息协议)进行通信。这允许我们在测试时,轻松地用模拟对象(Mock)替换真实模块。
- 状态外部化:将Agent的对话历史、执行状态等从内存中剥离,存入结构化的存储(如数据库)。这使得我们可以随时保存和加载某个特定的测试状态,实现测试的完全复现。
- 配置驱动:将Prompt模板、模型参数、工具配置等全部抽取为外部配置。评测时,我们可以固定所有配置,只改变需要测试的变量。
举个例子,旧代码中,工具调用可能是这样的(高度简化伪代码):
def handle_user_query(query, conversation_history): # 1. 拼接Prompt,内含历史、查询和可用工具描述 prompt = build_prompt(history=conversation_history, query=query, tools=get_all_tools()) # 2. 调用大模型 response = call_llm(prompt) # 3. 解析响应,可能包含工具调用指令 if “action” in response: tool_name = parse_tool_name(response) tool_args = parse_tool_args(response) # 4. 直接执行工具 result = execute_tool(tool_name, tool_args) # 5. 再次拼接Prompt,将结果反馈给模型...这段代码里,Prompt构建、模型调用、工具查找与执行全部耦合。重构后,我们将其拆解:
# 模块1:规划器 (Planner) class Planner: def plan(self, state: AgentState) -> Plan: # 返回一个包含下一步行动(如调用工具X)的计划对象 pass # 模块2:工具执行器 (ToolExecutor) class ToolExecutor: def execute(self, tool_call: ToolCall) -> ToolResult: # 纯执行,不关心上下文 pass # 模块3:状态管理器 (StateManager) class StateManager: def update(self, old_state: AgentState, action: Action, result: ActionResult) -> AgentState: pass # 主流程变得清晰且可测试 def agent_step(state: AgentState): plan = planner.plan(state) # 可单独测试Planner if plan.action == “call_tool”: result = tool_executor.execute(plan.tool_call) # 可单独测试Executor new_state = state_manager.update(state, plan.action, result) # 可单独测试StateManager return new_state通过这样的重构,评测就可以针对每个模块精准进行。我们可以给Planner注入不同的状态,测试其规划是否合理;可以给Executor一个模拟工具,测试其调用逻辑是否正确,而无需连接真实的、可能不稳定外部API。
3. 构建Agent评测体系:从单元到场景的立体化评估
重构只是打下了地基,真正的评测体系需要在这地基上建造起来。我们的评测体系是一个金字塔结构,从底层的单元测试,到中间层的集成与组件测试,再到顶端的端到端(E2E)场景测试。
3.1 单元测试:验证“齿轮”的精度
单元测试针对最小的代码单元(函数、类方法)。在Agent上下文中,这包括:
- 工具函数测试:清洗输入参数的函数、解析模型输出的函数、计算相似度的函数等。这些测试不依赖模型,是确定性的。
- 模块接口测试:确保每个模块(如Planner, Executor)的输入输出符合接口规范。例如,即使Planner内部逻辑复杂,我们也可以测试:当输入状态包含目标“查询天气”和位置“北京”时,它输出的Plan对象中,
action字段是否等于“call_tool”,tool_call.name是否等于“get_weather”。
注意:单元测试要避免测试“大模型本身的能力”。例如,不要测试“给定一个关于天气的提问,Planner能否生成正确的工具调用指令”,因为这依赖于模型的推理能力,是不确定且昂贵的。我们应该测试的是:给定一个明确的、结构化的“意图表示”(这是上游模块的输出),Planner的逻辑能否正确工作。这意味着我们需要在测试中“模拟”(Mock)上游的意图识别模块,给它提供我们预设的、确定的输入。
3.2 集成与组件测试:检查“传动装置”的协同
这一层测试模块之间的交互。我们使用大量的模拟(Mock)和桩(Stub)来替代外部依赖。
- 模拟大模型(LLM Mock):这是最关键的一步。我们构建了一个LLM Mock服务,它可以被配置为针对特定的输入Prompt,返回我们预先定义好的、确定的输出。例如,当测试“规划-执行”流程时,我们配置Mock:当收到包含“帮我订一张明天从上海到北京的机票”的Prompt时,永远返回一个结构化的JSON
{“action”: “call_tool”, “tool”: “book_flight”, “args”: {…}}。这样,我们就隔离了模型的不确定性,专注于测试我们自己的业务逻辑是否正确处理了这个结构化的决策。 - 模拟外部工具(Tool Mock):同样,我们模拟所有外部API。模拟的航班查询工具永远返回固定的航班列表;模拟的支付工具永远返回“支付成功”。这保证了测试的稳定性和速度,且能轻松模拟各种边界和异常情况(如网络超时、API返回错误码)。
- 测试覆盖的关键路径:
- 完整任务流:从用户输入,到多轮工具调用,到最终答案生成。全程使用Mock,验证状态流转是否正确。
- 错误处理与重试:模拟工具调用失败,验证Agent是否按预设策略(如重试、切换工具、向用户求助)进行处理。
- 上下文管理:测试Agent在多轮对话中,是否正确地记住和引用了历史信息。
我们为这些集成测试建立了数百个测试用例,每个用例都是一个独立的、可配置的“剧本”(Scenario),定义了初始状态、一系列模拟的交互和最终的期望状态。
3.3 端到端(E2E)场景测试:在“真实路况”下试车
单元和集成测试保证了内部逻辑的坚固,但Agent最终要面对真实世界。E2E测试就是在尽可能真实的环境下,对完整系统进行测试。这里我们无法完全Mock,但要有控制地引入真实组件。
使用稳定的模型版本:在E2E测试中,我们会连接一个真实的大模型API(如GPT-4),但固定其版本和参数(如temperature=0, top_p=1)。我们定期(例如每天)对一批核心场景进行E2E测试,记录成功率、延迟等指标,形成趋势图。这能帮助我们察觉因模型服务方更新带来的隐性变化。
沙盒环境:对于外部工具,我们搭建了沙盒(Sandbox)环境。例如,一个“发送邮件”的工具,我们配置它只发往一个内部的测试邮箱;一个“操作数据库”的工具,我们给它一个完全隔离的测试数据库。这样既能测试真实的集成,又不会产生副作用。
评测指标量化:E2E测试的输出不再是简单的“通过/失败”,而是一系列量化指标:
指标 说明 测量方法 任务完成率 在N个测试场景中,完全达成预设目标的比例。 人工或规则判定最终输出是否满足需求。 步骤效率 完成一个任务所需的平均对话轮数或工具调用次数。 记录轨迹并分析,越少越好。 工具调用准确率 在需要调用工具的场景中,正确调用工具且参数无误的比例。 对比实际调用与预期调用。 幻觉率 模型生成与提供工具/知识无关的错误信息的比例。 针对知识性任务进行判定。 平均响应时间 从用户提问到收到最终回答的平均耗时。 性能监控。 我们为每个指标设定了基线(Baseline)和警戒线。任何代码提交如果导致关键指标(如任务完成率)显著下降(例如超过5%),都会在CI/CD流水线中触发失败警报。
4. 评测基础设施:让31万行代码的回归测试在10分钟内完成
拥有成百上千个测试用例后,另一个工程挑战出现了:如何快速、可靠、低成本地运行它们?尤其是当测试需要调用(哪怕是Mock的)LLM接口时,传统的测试运行方式可能慢得无法接受。
4.1 测试执行引擎的定制化开发
我们没有直接使用标准的 pytest 套件来运行所有测试,因为我们需要更精细的控制和并行化。我们开发了一个轻量级的测试执行引擎,它的核心设计是:
- 描述式测试用例:每个测试用例不再是一段Python函数代码,而是一个YAML或JSON文件,描述测试场景。
这种描述式的方式让非工程师(如产品经理、标注人员)也能参与贡献测试场景,极大丰富了测试集。# 一个测试用例示例 id: test_weather_inquiry description: “测试简单天气查询任务” initial_state: user_query: “北京今天天气怎么样?” available_tools: [“get_weather”] mock_configurations: llm_mock: # 当收到包含“北京”“天气”的Prompt时,返回以下结构化动作 when_prompt_contains: [“北京”, “天气”] return_action: “call_tool” return_tool: “get_weather” return_args: {“city”: “北京”} tool_mock: “get_weather”: return: {“city”: “北京”, “condition”: “晴”, “temperature”: “22°C”} expectations: - agent_final_response_should_contain: [“晴”, “22°C”] - tool_called: “get_weather” - tool_called_with_args: {“city”: “北京”} - 并行执行与缓存:执行引擎可以并行运行数百个独立测试用例。更重要的是,它对LLM Mock的调用结果进行了缓存。因为Mock的响应是确定的(输入Prompt相同,输出必然相同),所以首次运行后,结果就被缓存。后续运行相同测试时,直接读取缓存,避免了不必要的模拟网络开销,使测试套件的运行时间从小时级缩短到分钟级。
- 动态测试集管理:引擎支持按标签(如
unit,integration,e2e,critical)筛选测试用例。在CI流水线中,每次代码提交会快速运行所有单元测试和关键集成测试(约1-2分钟)。每晚的定时任务则运行完整的测试集,包括耗时的E2E测试,并生成详细的评测报告。
4.2 持续集成/持续部署(CI/CD)流水线的深度集成
评测不是独立的活动,它必须融入开发流程。我们将评测体系深度集成到了GitLab CI/CD流水线中:
- 提交前检查(Pre-commit Hook):开发者提交代码前,自动运行代码风格检查和快速的单元测试。
- 合并请求(Merge Request)门禁:当发起一个合并请求时,CI流水线自动执行:
- 运行所有单元测试和集成测试。
- 运行受影响模块相关的组件测试。
- 如果改动涉及核心Agent逻辑,还会从核心场景测试集中抽样运行一部分E2E测试。只有所有测试通过,且关键业务指标(通过测试场景计算得出)未下降,代码才被允许合并。这从根本上防止了破坏性改动进入主分支。
- 每日健康报告:每日凌晨,流水线执行全量测试(包括连接真实模型和沙盒工具的E2E测试),生成一份包含所有量化指标趋势的报告,发送给团队。任何指标的异常波动都会高亮显示。
这套基础设施的建立,使得我们面对31万行重构代码时,心里有底。每次改动,都有成千上万个“数字质检员”在帮我们验证系统的稳定性。
5. 超越正确性:评测Agent的“智能”与“体验”
通过上述工程化手段,我们基本解决了Agent的“可靠性”和“稳定性”评测问题。但一个“不犯错”的Agent,未必是一个“好用”的Agent。这就进入了更主观、但也更关键的领域:如何评测Agent的“智能程度”和“用户体验”?
5.1 基于LLM-as-a-Judge的自动化评估
对于任务完成质量、回答的有用性、连贯性等难以用规则衡量的方面,我们引入了“LLM-as-a-Judge”模式。具体做法是:
- 对于一个测试场景,我们不仅定义“正确”的结果,还定义更详细的评估标准(Evaluation Rubric)。例如,对于一个旅行规划Agent,标准可能包括:“行程合理性”、“信息完整性”、“符合用户预算偏好”、“格式清晰度”。
- 在自动化测试中,当Agent运行完毕后,我们会将用户原始问题、Agent的实际回答、以及评估标准,一同提交给另一个大模型(我们通常使用比被测Agent更强的模型,如GPT-4),让它扮演裁判,根据标准对回答进行打分(例如1-5分)并提供简短的评语。
- 我们将这些分数和评语记录到数据库中,进行长期追踪。
实操心得:LLM-as-a-Judge的稳定性是关键。我们采取了以下措施:(1) 使用固定的、强大的模型作为裁判;(2) 设计清晰、无歧义的评估标准和提示词;(3) 对同一回答让裁判模型评估多次,取平均分以减少随机性;(4) 定期抽取样本进行人工复核,校准裁判模型的评分标准。虽然这仍然有主观成分,但相比完全人工评估,其规模化和一致性是数量级的提升。
5.2 人工评估与众包:获取真实用户反馈
自动化评测无法完全替代人的感受。我们建立了定期的人工评估机制:
- 内部专家评估:每周,产品、研发、测试的同事会组成评估小组,随机抽取一批最新的用户真实对话(脱敏后)或测试用例输出,从“任务完成度”、“回答质量”、“交互自然度”等多个维度进行打分和讨论。这能发现自动化测试无法捕捉的细微问题,比如语气生硬、逻辑跳跃等。
- 众包评估:对于更通用的能力,我们会将一批任务和Agent的响应发布到专业的众包平台(如Amazon Mechanical Turk),让真实用户按照我们的评估指南进行评分。这为我们提供了来自更广泛人群的、相对客观的用户体验数据。
5.3 可视化与可解释性:让评测结果“看得见”
评测的最终目的是指导改进。如果只有一堆数字和图表,工程师很难定位问题。因此,我们开发了配套的可视化调试工具。
- 轨迹查看器(Trace Viewer):对于任何一个测试用例(无论是通过的还是失败的),我们都可以打开一个Web界面,清晰地看到Agent执行的完整“思考轨迹”:它接收到的用户输入、内部的状态变化、每一步的规划决策、调用了什么工具(及参数)、工具返回的结果、以及最终生成的回答。这就像给Agent装了一个“黑匣子”,任何故障都可以回溯。
- 对比分析:工具支持将同一个测试用例在不同代码版本(例如重构前和重构后)下的执行轨迹并排对比,高亮显示差异。这让我们能直观地看到优化到底带来了哪些行为上的改变。
6. 重构与评测带来的实际收益与未来挑战
历时数月的重构与评测体系建设,带来的改变是深刻的。
6.1 可量化的收益
- 缺陷逃逸率降低:上线后由用户反馈的严重问题数量下降了70%以上。大部分逻辑错误在合并请求阶段就被拦截。
- 迭代速度加快:因为有了可靠的测试护栏,开发者敢于进行更激进的重构和优化,功能迭代的平均周期缩短了约30%。
- 性能基线清晰:我们首次能够明确地说出“我们的Agent在涵盖10大类、200个核心场景的测试集上,任务完成率为92.5%,平均响应时间为2.3秒”。这为产品规划和客户承诺提供了坚实依据。
- 团队协作改善:评测体系成为了产品、研发、测试团队的共同语言。产品需求可以更容易地转化为可测试的场景,测试结果也能直观地反馈给产品作为改进依据。
6.2 持续的挑战与下一步
当然,挑战依然存在:
- 评测集的完备性:如何设计一套真正能全面反映Agent复杂能力的评测集,仍然是一个开放问题。我们正在探索结合自动化生成(利用LLM生成边缘测试用例)和众包收集的方式,不断扩充和更新我们的“考题库”。
- 长上下文与长期记忆的评测:对于需要处理超长对话历史或具有长期记忆能力的Agent,如何设计有效的评测场景,是一个难点。
- 多模态与具身Agent:当Agent的感知和行动从纯文本扩展到图像、声音乃至物理世界时,评测的复杂度和成本将呈指数级上升。这是我们未来需要重点探索的方向。
回过头看,“不是靠Prompt”并非否定Prompt的重要性。恰恰相反,一套强大的评测体系,能让我们更科学、更高效地优化Prompt。它把原本玄学的“调参”和“炼丹”,变成了一个可观测、可度量、可复现的工程过程。31万行重构的代价不小,但它为我们换来的是一个更稳健、更透明、进化速度更快的Agent系统。对于任何志在构建生产级AI应用,而非仅仅做出一个演示Demo的团队来说,在Agent评测上的工程投入,迟早会成为那条区分“玩具”与“工具”的关键分水岭。