
最近折腾了 prompt-replay,这是一个帮你记录 LLM 调用结果、在升级模型后回放对比的库。踩了几个坑,这篇把核心用法说清楚。
你升级了模型,通义千问 2.1 到通义千问 2.5。新模型更快、更便宜、更聪明。你在周五下午上线。
周一来了一个 bug。用户反馈:负责汇总周报的智能体输出的 JSON 字段名变了,下游解析器静默出错。仪表盘直接空白了。
你没有任何测试覆盖这种情况。你根本不知道输出格式会漂移。模型没有报错,它只是"换了个想法"给字段起了不同的名字。
这正是 prompt-replay 要解决的问题。
这个库有两个核心组件:Recorder 和 Replayer。你在录制阶段用 @capture 装饰器包装 LLM 调用,然后换上新模型重放这些历史 prompt,对比结果差异。
from prompt_replay import Recorder, Replayer, capture
import anthropic client = anthropic.Anthropic() # Step 1: record a session
recorder = Recorder(session_path="sessions/weekly_summary.jsonl") @capture(recorder)
def call_llm(prompt: str) -> str: response = client.messages.create( model="claude-sonnet-4-5", max_tokens=1024, messages=[{"role": "user", "content": prompt}] ) return response.content[0].text # Run your normal workflow. @capture records every call.
with recorder: result = call_llm("Summarize this week's activity: ...") 录制完会话之后,换上新的模型配置,然后重放:
# Step 2: replay with a new model config
def new_llm(prompt: str) -> str: response = client.messages.create( model="claude-sonnet-4-6", # new model max_tokens=1024, messages=[{"role": "user", "content": prompt}] ) return response.content[0].text replayer = Replayer( session_path="sessions/weekly_summary.jsonl", fn=new_llm, diff_mode="json_diff" # or "exact" or "semantic"
) report = replayer.run() for diff in report.diffs: print(diff.summary()) 三种 diff 模式:
exact:纯字符串相等。适用于必须返回固定字符串的 prompt。json_diff:把两个响应都解析成 JSON 后对比结构。适用于输出结构化数据的智能体。semantic:计算 embedding 之间的余弦距离。需要你自己提供 embedder。适用于自然语言摘要类场景,逐字对比太严格了。semantic 模式接受任意可调用对象,签名是接收字符串返回浮点数列表:
replayer = Replayer( session_path="sessions/weekly_summary.jsonl", fn=new_llm, diff_mode="semantic", embedder=my_embed_fn, # any function: str -> list[float] threshold=0.05 # flag if cosine distance exceeds this
) get_weather("北京"),重放时不会再次调用那个工具。它只是重放 prompt,把新 LLM 的响应和录制的响应做对比。原因后面会详细说。replayer.run() 返回一个报告,怎么处理由你决定:打印出来、写文件、或者在 diff 超过阈值时抛异常。prompt-template-version 一起用。这是经过深思熟虑的设计决策,也是我花最多时间考虑的部分。
录制会话时,工具调用是对话历史的一部分。它们会出现在录制的消息里。重放时,这些工具调用结果会作为上下文存在,和原始运行时一样。Replayer 把同样的对话历史发给新模型,然后等待响应。
它不会重新调用工具。
原因很简单:在测试重放过程中重新执行工具可能会调用生产系统,这很危险。send_email、create_ticket、update_database、charge_card。这些操作执行两次可不是闹着玩的。如果重放测试工具包自动重新触发有副作用的工具,会造成真实的损失。
所以契约是:工具结果是冻结历史的一部分。模型根据这个历史给出响应。你在测试的是模型的响应有没有变化,而不是工具是否正常工作。
如果你的工作流重度依赖工具调用,而且真正关心的输出是工具调用序列而不是最终文本,那这个库不是你的菜。但如果真正关心的输出是模型在相同上下文和工具结果下的最终响应,这就是为你量身定做的。
# Internally, the Replayer reconstructs the conversation like this:
# [system, user_msg, tool_use, tool_result, user_msg, ...]
# It sends that full history to the new model and captures the response.
# The tool_result entries are from the original session. No tools are called. pip install prompt-replay 零依赖。没有捆绑任何 LLM SDK。自己带客户端。
GitHub: MukundaKatta/prompt-replay
50 个测试用例,全部通过。
| 库 | 边界 | 仓库 |
|---|---|---|
| agentsnap | 智能体工具调用轨迹的快照测试 | MukundaKatta/agentsnap |
| cachebench | Prompt 缓存命中率的监控 | MukundaKatta/cachebench |
| agent-decision-log | WHY 层:记录选了哪个选项以及为什么 | MukundaKatta/agent-decision-log |
| agenttrace | 每次智能体运行的成本和延迟追踪 | MukundaKatta/agenttrace |
agentsnap 是最相近的兄弟库。它快照工具调用序列。prompt-replay 快照模型产生的文本。它们覆盖智能体输出的不同部分。
计划里有这么几件事:
prompt-replay replay sessions/foo.jsonl --fn my_fn.py --diff json_diffreport.assert_no_regressions(threshold=0.1)核心循环——录、重放、对比——是稳定的。50 个测试覆盖了三种 diff 模式、@capture 装饰器的边界情况,以及对话历史重建逻辑。
原文链接:https://dev.to/mukundakattamada/prompt-replay-record-llm-outputs-today-replay-against-a-new-model-tomorrow-3l78