AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

Prompt、模型与工具版本追踪

我的学习 →

正在读取学习记录…

版本追踪是在每次运行时记录实际使用的 Prompt、模型、工具及配置。Prompt 决定任务和输出要求;模型决定怎样理解输入;工具提供政策、查询结果或外部动作。同一句“收货二十天想退货”,换了任意一个组件,结果都有可能改变。

一个直观例子:旧解析器只认识“20天”,看到“二十天”就转人工;新解析器认识中文数字,能得到 20。若政策工具只允许 7 天,它判为不符合;政策工具改成 30 天,结果变为符合。此时只记录“Prompt v2”就很难解释改变来自哪里。

把三个组件想成菜谱、厨师和食材批次。菜名一样,厨师换了、盐换了,追责时只拿菜谱版本实在委屈菜谱。一次运行应该绑定一份联合清单,而不只绑定一个“最新版”的名字。

版本名称与具体内容一起保存

程序的 p1/p2 是两种输出要求,m1/m2 是两个本地文本解析规则,t1/t2 是两份退货政策。运行清单保存三类引用、对应内容和配置,再用内容哈希生成指纹。

输出里的d是day的缩写,即天;20d vs 7d表示输入为20天、政策上限为7天。m1/m2的本地解析器按固定规则处理文本,便于观察版本如何参与计算;实际模型的供应商与版本信息需要另行采集。

哈希是把内容字节映射成摘要:内容相同应得到同一摘要,内容改变应使摘要改变。它能帮助检查记录是否变化;鉴别记录是否来自可信来源,还需要访问控制或签名。

Prompt p2、模型 m2、工具 t1的具体内容汇合成运行清单,内容哈希关联输入与输出
图1:一次结果绑定三个组件的快照;换一项组件后,记录成为另一份运行清单。

保存完整程序并运行

四次运行使用同一文本,每次只变一个引用。清单通过深拷贝保存嵌套内容,canonical() 对 JSON 键排序后计算 SHA-256;verify() 在重放前重新算摘要,检测内容被改写。

在 PowerShell 创建并进入文件夹:

Bash
mkdir l7-versions
cd l7-versions

把下面完整代码保存为该文件夹的 version_lab.py。数据已经写在程序中,直接保存即可运行。

Python
"""保存 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,再运行:

Bash
ls
python -X utf8 version_lab.py

程序输出:

TXT
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,确认两个文件都在当前目录,运行:

Bash
ls
python -X utf8 -m unittest -v test_version_lab.py
完整测试:test_version_lab.py
Python
"""检查三类版本都影响结果,旧清单可重放并拒绝篡改。"""

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 没变、模型没变、工具服务代码也没变,退款结果却变了,应该补查哪份政策数据?如果两次运行指纹相同但输入不同,为什么不能将它们当成同一次实验?

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.1.0

选择题 · 一条失败结果只保存模型名,缺少哪些复查材料

一条失败结果只保存模型名,缺少哪些复查材料?

对应知识点:L7-versions

选择一个答案
练习 2 / 4 · v1.1.0

简答题 · 解释机制

如何从失败条目回到当时的提示内容?

对应知识点:L7-versions

练习 3 / 4 · v1.1.0

代码题 · 核对给定数据

用断言比较两次运行的prompt、model、tool版本是否完全相同。

对应知识点:L7-versions

练习 4 / 4 · v1.1.0

场景题 · 分析具体变化

只升级工具字段名称,旧提示仍发送老字段,如何定位?

对应知识点:L7-versions

图片查看器
100%

文章配图 · 按 Esc 关闭