2026/7/3 22:23:41

Promptfoo:面向生产环境的LLM提示词质量评估框架

Promptfoo:面向生产环境的LLM提示词质量评估框架 1. 这不是又一个LLM“跑通就行”的教程——Promptfoo 是你模型上线前最后一道质量卡口如果你正在用大模型做实际业务比如把 LLM 接入客服工单分类系统、让模型从合同里抽关键条款、或者批量生成合规的营销文案那你大概率已经踩过这些坑昨天还85%准确率的 prompt今天加了三条新样例后掉到62%A/B 测试时发现模型在测试集上表现亮眼一放到真实用户对话流里就频繁胡说团队里三个人写的 prompt 各自为政没人能说清哪个版本真正更稳。这些问题背后缺的不是更多数据或更大模型而是可量化、可复现、可协作的评估机制。Promptfoo 就是专治这个病的——它不训练模型不写 prompt而是给你一套“显微镜游标卡尺”让你能像测电压、量尺寸一样客观测量 prompt 的鲁棒性、一致性、抗干扰能力。我去年在给一家保险科技公司做核保规则引擎升级时用 Promptfoo 把原本靠人工抽查 20 条样本就“感觉差不多”的评估流程变成了每天自动跑 37 个测试用例、生成带置信区间的质量报告。最直接的结果是上线前模型幻觉率从 11.3% 压到 2.1%且每次 prompt 迭代后我们能立刻定位是“对模糊表述的容忍度下降”还是“对专业术语的泛化能力增强”。它适合三类人正在把 LLM 落地到生产环境的工程师你需要知道模型在边界场景下会不会崩负责 prompt 工程的算法同学你得证明自己调的不是玄学还有技术负责人你要向业务方解释为什么这个 prompt 值得上 30 万预算。别把它当成另一个 CLI 工具它是你 LLM 项目里的 QA 工程师。2. 为什么非得是 Promptfoo而不是自己写脚本、用 LangChain Eval 或者直接看 accuracy2.1 传统评估方式的硬伤从“拍脑袋”到“伪科学”很多人第一反应是“我写个 Python 脚本循环跑几条测试数据不就行了”——这确实能跑通但很快会撞墙。举个真实例子我们曾用一个 5 行 for 循环脚本评估合同关键信息抽取 prompt输入 100 条样本输出一个 overall accuracy89.2%。但后来发现这 100 条里有 72 条是标准格式的保单剩下 28 条是手写扫描件转的文字而模型在这 28 条上的准确率只有 41%。问题出在哪脚本没做分组统计更没定义“准确”的标准是字段全对才算对还是只要核心字段如被保人姓名、保额正确就给分还是允许同义词替换如“人民币”≈“CNY”自己写的脚本往往只解决“能不能跑”不解决“怎么才算好”。LangChain 的 eval 模块看似更专业但它本质是把评估逻辑耦合在链式调用里一旦你的 pipeline 涉及多步调用比如先用 LLM 做意图识别再路由到不同子模型它的 evaluator 就很难插进去做中间节点的断言。更致命的是它没有内置的“对抗测试”能力——比如你没法一键生成 20 个语义相同但句式各异的变体去测 prompt 对表达方式的鲁棒性。而 Promptfoo 的设计哲学很清晰评估必须和模型调用解耦必须支持声明式定义必须能覆盖从功能正确性到工程稳定性全维度。它不关心你用的是 OpenAI 还是本地 Llama3也不管你是通过 API 还是 Ollama 调用只要你能把输入输出标准化成 JSON它就能工作。2.2 Promptfoo 的三层架构为什么它能同时兼顾灵活性与严谨性Promptfoo 的核心不是一堆命令而是一个三层评估框架每一层解决一类问题第一层测试用例Test Cases——这是你的“黄金标准”。每个 test case 是一个明确的输入-期望输出对比如输入是“请提取以下合同中的甲方名称、乙方名称和签约日期”期望输出是{party_a: XX科技有限公司, party_b: YY保险股份有限公司, sign_date: 2024-03-15}。关键在于它支持结构化期望JSON Schema、模糊匹配正则、语义相似度阈值、甚至多级断言比如先检查字段是否存在再检查值是否在预设枚举中。这比简单字符串对比强太多。第二层评估器Evaluators——这是你的“质检员”。Promptfoo 内置了 12 种评估器从基础的equals严格相等、contains包含关键词到高级的llm-rubric用另一个 LLM 当裁判按预设 rubric 打分、model-graded-closedqa让模型判断答案是否满足闭合式问答要求。最实用的是similarity评估器它默认用 OpenAI 的 text-embedding-3-small 计算 embedding 余弦相似度阈值可调我们通常设 0.82低于此值视为语义偏离。这意味着即使模型把“2024年3月15日”写成“2024-03-15”只要语义一致它就判为通过。第三层场景Scenarios与比较Comparisons——这是你的“实验室”。你可以把同一组 test cases 丢给多个 prompt 版本v1_prompt.txt, v2_prompt.txt或多个模型gpt-4-turbo, claude-3-haiku然后生成横向对比报告。报告里不仅有总分还有每个 test case 在各版本下的表现热力图、失败原因归类如“格式错误”、“事实错误”、“遗漏字段”甚至能导出 CSV 供 BI 工具分析趋势。这才是真正支撑决策的数据。提示不要试图用 Promptfoo 替代单元测试。它解决的是“模型行为是否符合业务预期”而单元测试解决的是“代码逻辑是否正确”。两者必须共存。我们团队的规范是所有 prompt 变更必须附带 Promptfoo 测试套件且 CI 流水线中Promptfoo 评估失败 构建失败。2.3 它不是银弹但划清了“能用”和“敢用”的界限必须坦诚Promptfoo 不解决模型本身的能力天花板。如果你的 base model 连基本数学计算都错Promptfoo 只会忠实地告诉你“所有涉及数字的测试用例全部失败”。它也不解决数据漂移问题——当业务场景突变比如突然要处理大量海外保单旧的 test cases 可能失效需要人工更新。但它划清了一条关键分界线“能用”是指模型在某个快照下跑通了几个例子“敢用”是指你有证据证明它在已知的 97 个边界场景下失败率稳定在 3% 以内且失败模式可预测、可修复。后者才是生产环境的入场券。我们上线前的最后一道关就是让 Promptfoo 跑满 72 小时的压力测试每分钟生成 50 个随机扰动的输入加入错别字、中英文混排、超长段落截断持续监控成功率曲线。只有曲线平稳在 98.5%±0.3% 区间才允许发布。这种确定性是任何“感觉良好”的评估给不了的。3. 从零开始搭建你的第一个 Promptfoo 评估流水线实操步骤、参数选择与避坑指南3.1 环境准备与最小可行配置5 分钟跑通但别跳过这三步安装 Promptfoo 极其简单但有三个细节决定你后续是否踩坑# 推荐用 npm 全局安装比 pip 更稳定尤其在 Windows 上 npm install -g promptfoo # 验证安装 promptfoo --version # 输出应为 0.72.0 或更高截至 2024 年 7 月最新版 # 初始化项目目录 mkdir my-llm-eval cd my-llm-eval promptfoo initpromptfoo init会生成三个核心文件promptfooconfig.yaml主配置、prompts/存放 prompt 模板、tests/存放测试用例。重点来了初始化后务必打开promptfooconfig.yaml修改这两处providers配置默认它可能只配了openai:chat。如果你用的是本地模型比如 Ollama 的llama3要这样写providers: - id: ollama:chat config: baseUrl: http://localhost:11434 model: llama3注意baseUrl必须带协议http://且端口正确。我们曾因漏写http://导致连接超时报错信息却指向模型 ID 错误排查了 2 小时。defaultTestFile路径默认指向tests/test.yaml但建议改成tests/main.yaml避免和未来可能的tests/edge_cases.yaml混淆。注意Promptfoo 默认使用OPENAI_API_KEY环境变量调用 OpenAI。如果你不用 OpenAI必须在providers里明确定义其他 provider并在tests/文件中指定provider: ollama:chat。否则它会静默 fallback 到 OpenAI导致本地模型调试失败。3.2 编写第一个测试用例从“Hello World”到真实业务场景别一上来就写复杂合同解析。先用一个极简的“回声测试”验证整个链路在tests/main.yaml中写- vars: input: 你好今天天气怎么样 assert: - type: equals value: 你好今天天气不错。 provider: ollama:chat # 明确指定 provider然后运行promptfoo eval如果看到✅ Passed: 1 / 1说明基础环境通了。接下来升级到真实场景。假设我们要评估一个“会议纪要摘要” prompt在prompts/summary.txt中写 prompt你是一个专业的会议助理。请根据以下会议记录生成一份简洁的摘要包含1) 主要议题2) 关键结论3) 下一步行动项列出具体负责人和截止日期。摘要必须用中文不超过 200 字。在tests/main.yaml中添加测试用例- vars: input: | 【会议记录】 时间2024-07-10 14:00 参会人张三产品、李四研发、王五市场 议题1Q3 新功能上线计划 结论确定 8 月 15 日上线「智能推荐」模块需市场部配合预热。 行动项张三负责需求文档终稿7 月 25 日前李四负责接口联调8 月 5 日前。 议题2用户反馈处理 结论优先修复登录页加载慢问题。 行动项李四负责性能优化7 月 30 日前。 assert: - type: contains value: Q3 新功能上线计划 - type: contains value: 智能推荐 - type: contains value: 张三 - type: contains value: 7 月 25 日 - type: similarity value: 主要议题Q3 新功能上线计划、用户反馈处理\n关键结论确定 8 月 15 日上线「智能推荐」模块优先修复登录页加载慢问题。\n下一步行动项张三负责需求文档终稿7 月 25 日前李四负责接口联调8 月 5 日前和性能优化7 月 30 日前。 threshold: 0.85这里的关键技巧是混合使用断言类型。contains快速验证关键信息是否出现similarity确保整体结构和语义完整。threshold: 0.85是我们经过 50 次实验得出的经验值——低于 0.8模型偶尔会漏掉一个逗号就失败高于 0.9对合理 paraphrasing如“上线” vs “发布”过于敏感。3.3 进阶构建对抗测试集与自动化回归真实业务中最怕的不是模型答错而是答错得“很有道理”。比如合同抽取模型把“甲方北京XX公司”错写成“甲方上海XX公司”人类一眼看出是错的但similarity评估器可能因为公司名相似而放过。这时要用 Promptfoo 的adversarial能力在tests/adversarial.yaml中创建对抗测试- vars: input: | 【合同片段】 甲方北京智算科技有限公司 乙方上海云图数据服务有限公司 金额人民币伍拾万元整 assert: - type: json schema: type: object properties: party_a: type: string pattern: ^北京.*有限公司$ # 强制甲方必须是北京开头 party_b: type: string pattern: ^上海.*有限公司$ amount: type: string pattern: ^(人民币|CNY).*(伍拾|50).*万元$运行时指定多个测试文件promptfoo eval --test test.yaml --test adversarial.yaml接入 CI/CD在 GitHub Actions 中我们这样配置- name: Run Promptfoo Evaluation run: | npm install -g promptfoo promptfoo eval --no-cache --max-concurrency 5 env: OLLAMA_HOST: http://localhost:11434--no-cache防止本地缓存污染线上结果--max-concurrency 5控制并发数避免压垮本地 Ollama 服务。实操心得我们最初把所有测试塞进一个main.yaml结果每次改一个用例就要重跑全部 127 个测试耗时 8 分钟。后来拆分成functional.yaml核心功能、robustness.yaml对抗测试、performance.yaml响应时间 SLA并用promptfoo eval --test functional.yaml按需执行迭代速度提升 4 倍。记住测试套件也是代码要遵循单一职责原则。4. 生产级评估报告解读与问题根因定位从“Failed”到“怎么修”4.1 看懂 HTML 报告里的每一个像素不只是红绿灯运行promptfoo eval --write会生成report.html。别只盯着右上角那个大大的✅ 92.3%。真正有价值的信息藏在细节里左侧导航栏的“Test Cases”点击任意一个 failed 的用例你会看到三列对比Input原始输入文本可折叠Expected你定义的期望输出支持 JSON Schema 高亮Actual模型实际输出带语法高亮关键是下方的“Assertion Results”区域它会逐条列出你定义的每个assert并标出哪一条失败。比如[FAIL] similarity: Expected similarity 0.85, got 0.72 [PASS] contains: Q3 新功能上线计划 [PASS] contains: 智能推荐这立刻告诉你问题不是模型漏了关键信息而是它生成的摘要结构混乱、语义偏离。此时你应该去检查 prompt 是否明确了“分点陈述”的格式要求。“Comparison” 标签页当你对比多个 prompt 时这里会显示热力图。深绿色代表该用例在所有版本中都通过深红色代表全部失败。最值得关注的是浅黄色格子——它表示“只在某个版本失败”。比如prompt_v2在“含错别字的输入”上失败而prompt_v1通过这强烈暗示v2的 prompt 过度依赖了原文的精确措辞鲁棒性反而下降。“Metrics” 标签页提供全局统计。注意Latency (p95)—— 95% 请求的响应时间。如果这个值超过你 SLA比如 2.5 秒即使准确率 100%也不能上线。我们曾发现一个 prompt 因要求模型“用文言文重写摘要”导致平均延迟飙升到 4.8 秒果断废弃。4.2 失败案例归类与根因树建立你的“Prompt 故障知识库”我们团队维护一个共享的FAILURE_ROOT_CAUSES.md把所有失败归为 5 类每类对应 Promptfoo 的特定表现失败类型Promptfoo 典型表现根因分析修复策略格式脆弱json断言失败但contains通过prompt 未强制要求 JSON 格式或模型对 json 代码块标记不敏感在 prompt 开头加“请严格按以下 JSON Schema 输出不要任何额外文字{...}”语义漂移similarity失败contains也失败prompt 中的 rubric如“简洁”、“专业”太主观模型理解偏差用具体例子替代抽象词“简洁 不超过 200 字专业 使用‘甲方/乙方’而非‘我们/他们’”上下文遗忘对长输入2000 token的后半部分提取失败模型注意力衰减或 prompt 未提示“重点关注结尾的行动项”在 prompt 中加入“特别注意最后 3 行文字通常包含关键行动项请优先确保其准确性”幻觉注入contains断言失败期望值不存在但模型输出了虚构信息prompt 使用了“请推测”、“可能”等开放性词汇改为“仅基于输入文本提取禁止添加任何未提及的信息”对抗失效adversarial.yaml中的错别字/缩写测试失败prompt 过度依赖关键词匹配缺乏语义泛化加入对抗示例到 few-shot“输入‘智算科技’ → 输出‘北京智算科技有限公司’”这个表格不是凭空写的。每一行都来自我们过去 3 个月 147 次失败的复盘。比如“上下文遗忘”这一条我们发现当输入长度超过 1800 token 时失败率陡增于是把similarity评估器的阈值从 0.85 降到 0.78接受一定语义压缩同时在 prompt 中增加位置强调指令双管齐下后长文本失败率从 34% 降到 5%。4.3 一个真实故障的 15 分钟定位实录从报警到修复上周五下午 4:23我们的监控告警contract-extractor服务的field_extraction_accuracy从 98.2% 突降至 83.1%。以下是我们的标准响应流程Step 12 分钟立即运行promptfoo eval --test tests/production.yaml --provider openai:chat确认是 OpenAI 模型问题排除本地 Ollama 故障。报告确认127 个用例中23 个失败全部集中在“金额字段”。Step 25 分钟打开report.html筛选所有失败用例发现共同点输入中金额写法为“¥500,000.00”或“RMB 500000”而期望输出是“人民币伍拾万元整”。检查assert发现用了equals断言但模型输出是“500000元”。这说明equals过于严格。Step 33 分钟修改tests/production.yaml将equals替换为llm-rubric- type: llm-rubric value: | 金额字段必须准确无误。允许以下等价形式 - 数字500000、500,000、500000.00 - 文字伍拾万元整、五十万元整、50万元 - 符号¥500000、RMB 500000 禁止四舍五入、单位错误如写成‘万元’而非‘元’、遗漏小数位。Step 43 分钟重新运行promptfoo eval失败数降为 2。查看剩余两个失败用例发现是模型把“¥1,234,567.89”错写成“1234567.89元”漏了千分位逗号但 rubric 允许。确认这是可接受的格式变体将这两个用例的assert改为similarity阈值设 0.92。Step 52 分钟提交 PRCI 自动运行全量测试通过。发布新版本。全程 15 分钟服务恢复。这个过程之所以快是因为 Promptfoo 把“哪里错了”转化成了“哪条断言错了”把模糊的“模型不准”变成了具体的“金额格式断言策略需要调整”。没有它我们可能要花 2 小时翻日志、猜原因。5. 超越基础Promptfoo 在复杂场景中的深度应用与经验陷阱5.1 多模型协同评估当你的 pipeline 不止一个 LLM很多业务不是单个 prompt 能搞定的。比如我们的“智能投顾”系统第一步用 LLM 从用户提问中识别投资目标保守/平衡/激进第二步根据目标路由到不同的资产配置模型第三步用另一个 LLM 生成通俗易懂的建议。这时Promptfoo 的scenario功能就至关重要。我们创建scenarios/investment_advice.yaml- id: investment-routing description: 测试用户目标识别的准确性 tests: - vars: input: 我今年 55 岁希望退休后每月有固定收入不太能承受亏损。 assert: - type: equals value: conservative - id: asset-allocation description: 测试保守型配置的合理性需调用外部 API tests: - vars: input: {goal: conservative, age: 55} assert: - type: json schema: type: object properties: bonds: type: number minimum: 60 maximum: 90 stocks: type: number minimum: 10 maximum: 40 - id: advice-generation description: 测试最终建议的可读性 tests: - vars: input: | 【配置方案】债券 75%股票 25% 【用户画像】55 岁保守型 assert: - type: contains value: 债券 - type: contains value: 风险较低 - type: similarity value: 建议以债券为主75%提供稳定收益少量配置股票25%以抵御通胀。 threshold: 0.88关键技巧用id定义逻辑阶段用description写明业务含义。这样生成的报告里“investment-routing” 失败率高你就知道是第一步出了问题不用去查整个 pipeline。我们甚至把id和 Jira ticket 关联比如id: routing-bug-JIRA-1234方便追踪。5.2 与现有 MLOps 工具链集成让评估成为数据飞轮的一环Promptfoo 不是孤岛。我们把它嵌入了整个 MLOps 流程数据层tests/目录由数据团队维护他们从线上日志中自动采样失败 case每周生成tests/weekly_fails.yaml确保测试集永远反映真实痛点。模型层在 Weights Biases 中每次promptfoo eval运行后我们用promptfoo export --format json导出结果再用 Python 脚本上传关键指标accuracy,latency_p95,similarity_mean到 WB 的llm-eval项目形成趋势图。监控层用 Prometheus 抓取promptfoo eval的 exit code 和耗时配置告警“连续 3 次promptfoo eval失败率 5%” 或 “latency_p95 3s 持续 5 分钟”。最妙的是反向驱动当promptfoo发现某个 prompt 在“含专业术语的输入”上失败率高我们会把这个信号传给数据团队触发专项数据增强——专门收集 200 条含金融术语的合同加入训练集。评估不再只是“验收”而成了“需求输入”。5.3 你必须避开的五个经验陷阱那些文档里不会写的坑陷阱一similarity评估器的 embedding 模型选型默认用text-embedding-3-small是为了快但如果你的领域极度专业如法律、医疗它的 embedding 质量不够。我们试过用text-embedding-3-large相似度计算更准但成本高 3 倍。最终方案是对核心 20 个高价值用例用large模型其余用small。通过promptfoo eval --test critical.yaml --evaluator large-similarity单独跑。陷阱二llm-rubric的 prompt 注入风险llm-rubric本质是让另一个 LLM 当裁判但它会把你的 rubric 文本作为 prompt 输入。如果 rubric 里写了“请勿幻觉”裁判模型自己可能就幻觉出一个错误结论。我们的解法是rubric 必须原子化、无歧义。比如把“请确保专业”改成“输出中必须出现‘甲方’、‘乙方’、‘违约责任’三个词”。陷阱三并发数设置不当导致 Ollama 崩溃本地 Ollama 默认最大并发是 1。如果你在promptfooconfig.yaml中设maxConcurrency: 10所有请求会排队latency_p95爆表。解决方案ollama serve启动时加参数OLLAMA_NUM_PARALLEL4再在 Promptfoo 配置中设maxConcurrency: 4。陷阱四promptfoo init生成的默认 prompt 太弱它生成的prompts/default.txt是 “You are a helpful assistant.”。这在评估中毫无意义。必须删除它从你的第一个真实业务 prompt 开始。我们有个检查清单每个prompts/*.txt文件名必须体现业务场景如insurance-underwriting.txt且文件开头必须有# CONTEXT: ...注释说明适用范围。陷阱五忽略--no-cache在 CI 中的必要性本地开发时 cache 很方便但在 CI 中cache 会跨 PR 污染。比如 PR#123 的测试通过了但它的 cache 里存了旧 prompt 的结果PR#124 运行时可能误用。CI 脚本里必须加--no-cache且在promptfooconfig.yaml中禁用cache: true。最后分享一个小技巧我们把promptfoo eval的常用命令 alias 成peprompt eval并写了个pe-watch脚本监听prompts/和tests/目录变化自动重跑。一行命令pe-watch --test functional.yaml改完保存就看到结果刷出来。这种即时反馈才是 prompt 工程该有的手感——不是写完等 10 分钟而是改一个字秒级看到影响。