Prompt、模型与工具版本追踪
正在读取学习记录…
版本追踪是在每次运行时记录实际使用的 Prompt、模型、工具及配置。Prompt 决定任务和输出要求;模型决定怎样理解输入;工具提供政策、查询结果或外部动作。同一句“收货二十天想退货”,换了任意一个组件,结果都有可能改变。
一个直观例子:旧解析器只认识“20天”,看到“二十天”就转人工;新解析器认识中文数字,能得到 20。若政策工具只允许 7 天,它判为不符合;政策工具改成 30 天,结果变为符合。此时只记录“Prompt v2”就很难解释改变来自哪里。
把三个组件想成菜谱、厨师和食材批次。菜名一样,厨师换了、盐换了,追责时只拿菜谱版本实在委屈菜谱。一次运行应该绑定一份联合清单,而不只绑定一个“最新版”的名字。
版本名称与具体内容一起保存
程序的 p1/p2 是两种输出要求,m1/m2 是两个本地文本解析规则,t1/t2 是两份退货政策。运行清单保存三类引用、对应内容和配置,再用内容哈希生成指纹。
输出里的d是day的缩写,即天;20d vs 7d表示输入为20天、政策上限为7天。m1/m2的本地解析器按固定规则处理文本,便于观察版本如何参与计算;实际模型的供应商与版本信息需要另行采集。
哈希是把内容字节映射成摘要:内容相同应得到同一摘要,内容改变应使摘要改变。它能帮助检查记录是否变化;鉴别记录是否来自可信来源,还需要访问控制或签名。
保存完整程序并运行
四次运行使用同一文本,每次只变一个引用。清单通过深拷贝保存嵌套内容,canonical() 对 JSON 键排序后计算 SHA-256;verify() 在重放前重新算摘要,检测内容被改写。
在 PowerShell 创建并进入文件夹:
mkdir l7-versions
cd l7-versions把下面完整代码保存为该文件夹的 version_lab.py。数据已经写在程序中,直接保存即可运行。
"""保存 Prompt、模型与工具的联合清单,用固定退款文本逐项切换版本。"""
from copy import deepcopy
import hashlib
import json
import re
PROMPTS = {
"p1": {"instruction": "判断退货资格", "include_reason": False},
"p2": {"instruction": "判断退货资格并说明政策依据", "include_reason": True},
}
MODELS = {
"m1": {"name": "local-parser-1", "chinese_twenty": False},
"m2": {"name": "local-parser-2", "chinese_twenty": True},
}
TOOLS = {
"t1": {"schema": "return-policy-v1", "max_days": 7, "policy_id": "book-7"},
"t2": {"schema": "return-policy-v2", "max_days": 30, "policy_id": "book-30"},
}
def canonical(value):
# 键排序后同一份内容得到相同字节,摘要不受键插入顺序影响。
return json.dumps(value, ensure_ascii=False, sort_keys=True,
separators=(",", ":")).encode("utf-8")
def snapshot(prompt, model, tool):
# 拷贝具体内容,后续别名或目录改变也不会改写这次运行的记录。
manifest = {"refs": {"prompt": prompt, "model": model, "tool": tool},
"prompt": deepcopy(PROMPTS[prompt]),
"model": deepcopy(MODELS[model]),
"tool": deepcopy(TOOLS[tool]),
"config": {"temperature": 0, "parser_revision": "code-1"}}
manifest["fingerprint"] = hashlib.sha256(canonical(manifest)).hexdigest()
return manifest
def verify(manifest):
saved = deepcopy(manifest)
fingerprint = saved.pop("fingerprint")
if hashlib.sha256(canonical(saved)).hexdigest() != fingerprint:
raise ValueError("manifest content changed")
def run(text, manifest):
verify(manifest)
# 两个本地解析器有确定的能力差异,每个版本真的参与计算。
normalized = text
if manifest["model"]["chinese_twenty"]:
normalized = normalized.replace("二十", "20")
match = re.search(r"(\d+)天", normalized)
days = int(match.group(1)) if match else None
policy = manifest["tool"]
decision = ("needs_review" if days is None else
"eligible" if days <= policy["max_days"] else "ineligible")
result = {"decision": decision}
if manifest["prompt"]["include_reason"]:
result["reason"] = ("days_unknown" if days is None else
f"{days}d vs {policy['max_days']}d")
result["policy_id"] = policy["policy_id"]
return result
def main():
text = "收货二十天想退货"
for refs in (("p1", "m1", "t1"), ("p2", "m1", "t1"),
("p2", "m2", "t1"), ("p2", "m2", "t2")):
manifest = snapshot(*refs)
print(f"{'/'.join(refs)} fingerprint={manifest['fingerprint'][:12]} "
f"result={json.dumps(run(text, manifest), ensure_ascii=False, sort_keys=True)}")
if __name__ == "__main__":
main()保存后列出文件,确认能看到 version_lab.py,再运行:
ls
python -X utf8 version_lab.py程序输出:
p1/m1/t1 fingerprint=5a06f68646cc result={"decision": "needs_review"}
p2/m1/t1 fingerprint=469f5dc5e4f9 result={"decision": "needs_review", "policy_id": "book-7", "reason": "days_unknown"}
p2/m2/t1 fingerprint=e084e5490b87 result={"decision": "ineligible", "policy_id": "book-7", "reason": "20d vs 7d"}
p2/m2/t2 fingerprint=9b7819e36992 result={"decision": "eligible", "policy_id": "book-30", "reason": "20d vs 30d"}逐项读懂结果
第一行 p1/m1/t1 得到 needs_review(需要人工核对):m1 找不到阿拉伯数字,days 为 None。p1 没要求理由,结果只包含 decision。
第二行只把 p1 改成 p2,仍需要人工,但多了 reason=days_unknown 和 policy_id=book-7。这说明输出结构确实由 Prompt 配置控制,模型的解析能力没有变化。
第三行再把 m1 改为 m2。m2 先把“二十”替换为“20”,得到 20 天;工具 t1 允许 7 天,结果是 ineligible(不符合)。理由 20d vs 7d 能在程序的比较式里找到。
第四行只变工具到 t2,允许 30 天,结果成为 eligible(符合)。这两行把模型理解变化和业务政策变化分开了。
fingerprint 显示完整 SHA-256 的前 12 个字符,便于阅读;清单中保存完整摘要。p1到p2、m1到m2、t1到t2的每次变化都会产生新指纹。
改一处,再看对应结果
在 main() 中,把 text = "收货二十天想退货" 改为 text = "收货5天想退货",运行 python -X utf8 version_lab.py。四个组合都能解析 5 天,四行的 decision 都变成 eligible;p1仍不输出理由,p2仍输出政策依据。
观察指纹:它们和原输出相同,因为这份指纹标识组件清单,输入改变由单独的运行记录承载。保存真实运行时,应同时记录输入摘要、清单指纹、Trace ID 与输出,别把组件哈希误当成整个请求的编号。完成后恢复文本并运行测试。
为何固定 latest 还不够
latest 或 champion 是可移动别名。别名今天指向版本2,明天可能指向版本3;重放时需要当时解析出的具体版本。MLflow 的模型注册表文档介绍了具体版本与可变别名的区分。
真实模型还应记录供应商、确切模型标识、可用的响应版本信息和生成参数;即使这些字段相同,远程服务的非确定性也可能让输出不同。记录版本能说明使用过什么,不保证每次得到相同文字。
工具记录也应涵盖接口 Schema、实现版本和数据快照。例如查政策服务代码没变、数据库政策从7天变30天,结果仍会改变。程序把固定政策内容直接保存在清单里,因此旧快照能够重放。
若只改变 p2 后发现表现不同,同时又悄悄升级 m2和t2,就失去单变量对照。可以先复用旧三件套验证基线,再逐项切换;涉及真实写操作时,重放要接回放执行器,避免第二次退款。
保存并运行测试
6 项测试检查三类组件都参与结果、键顺序不影响摘要、不同组件改变指纹、目录修改不影响旧快照、篡改被拒绝与未知版本引用失败。
把下面代码保存为同一文件夹的 test_version_lab.py,确认两个文件都在当前目录,运行:
ls
python -X utf8 -m unittest -v test_version_lab.py完整测试:test_version_lab.py
"""检查三类版本都影响结果,旧清单可重放并拒绝篡改。"""
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import version_lab as lab
class VersionTests(unittest.TestCase):
def test_three_version_changes(self):
configs = [("p1", "m1", "t1"), ("p2", "m1", "t1"),
("p2", "m2", "t1"), ("p2", "m2", "t2")]
results = [lab.run("收货二十天想退货", lab.snapshot(*c)) for c in configs]
self.assertNotIn("reason", results[0])
self.assertEqual(results[1]["reason"], "days_unknown")
self.assertEqual(results[2]["decision"], "ineligible")
self.assertEqual(results[3]["decision"], "eligible")
def test_hash_is_independent_of_key_order(self):
self.assertEqual(lab.canonical({"b": 2, "a": 1}),
lab.canonical({"a": 1, "b": 2}))
def test_changed_component_changes_fingerprint(self):
first = lab.snapshot("p2", "m2", "t1")
second = lab.snapshot("p2", "m2", "t2")
self.assertNotEqual(first["fingerprint"], second["fingerprint"])
def test_old_snapshot_survives_registry_update(self):
manifest = lab.snapshot("p2", "m2", "t1")
original = lab.TOOLS["t1"]["max_days"]
try:
lab.TOOLS["t1"]["max_days"] = 99
self.assertEqual(lab.run("收货20天", manifest)["decision"], "ineligible")
finally:
lab.TOOLS["t1"]["max_days"] = original
def test_modified_snapshot_is_rejected(self):
manifest = lab.snapshot("p2", "m2", "t1")
manifest["tool"]["max_days"] = 99
with self.assertRaises(ValueError):
lab.run("收货20天", manifest)
def test_unrecognized_version_is_rejected(self):
with self.assertRaises(KeyError):
lab.snapshot("latest", "m1", "t1")
if __name__ == "__main__":
unittest.main()继续查文档
阅读 MLflow 的具体模型版本与 aliases,留意别名能够独立改指向。对照 Python hashlib 的字节输入:摘要计算需要明确编码和序列化,不能用字典打印样式充当稳定协议。
课后练习展开习题收起习题
如果 Prompt 没变、模型没变、工具服务代码也没变,退款结果却变了,应该补查哪份政策数据?如果两次运行指纹相同但输入不同,为什么不能将它们当成同一次实验?
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 一条失败结果只保存模型名,缺少哪些复查材料
一条失败结果只保存模型名,缺少哪些复查材料?
对应知识点:L7-versions
简答题 · 解释机制
如何从失败条目回到当时的提示内容?
对应知识点:L7-versions
代码题 · 核对给定数据
用断言比较两次运行的prompt、model、tool版本是否完全相同。
对应知识点:L7-versions
场景题 · 分析具体变化
只升级工具字段名称,旧提示仍发送老字段,如何定位?
对应知识点:L7-versions
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。