ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
+330
View File
@@ -0,0 +1,330 @@
"""
实验 9-3:基于失败轨迹的系统提示词自动优化
一条命令跑通完整流程:
1. 用【初始 prompt】评测 → 暴露"政策争议就转人工"的过度转接问题;
2. 从失败轨迹生成三维诊断,保留来源案例;
3. Coding Agent 生成候选 prompt 的最小 diff
4. 用边界集与保留集决定候选版本是否可灰度发布;
5. 与人工调优版对照。
python demo.py # 完整运行:10 个用例 × 3 份 prompt
python demo.py --quick # 快速演示:每组只取 2 个用例,省时省钱
python demo.py --help # 查看全部命令行参数(中文说明)
"""
import argparse
import json
import os
import shutil
import sys
import time
from datetime import datetime, timezone
from evaluate import evaluate_prompt
from coding_agent import optimize_prompt
from config import (
get_api_turns,
get_backend_metadata,
get_provider,
get_model,
reset_api_turns,
usage_summary,
)
from airline_env import CASES
from learning_signal import diagnose_failures, format_learning_signal
from release_gate import build_candidate_manifest, evaluate_release_gate
GROUPS = ("holdout", "boundary")
HERE = os.path.dirname(os.path.abspath(__file__))
INITIAL_PROMPT = os.path.join(HERE, "prompts", "system_prompt.txt")
MANUAL_PROMPT = os.path.join(HERE, "prompts", "system_prompt_manual.txt")
WORKING_PROMPT = os.path.join(HERE, "runtime", "system_prompt_working.txt")
def _read(path):
with open(path, "r", encoding="utf-8") as f:
return f.read()
def _pct(cn):
c, n = cn
return f"{c}/{n} ({100 * c / n:.0f}%)" if n else "-"
def print_table(rows):
"""rows: list of (label, holdout_tuple, boundary_tuple)"""
print("\n" + "=" * 74)
print("正确率对比(保留任务集 = 既有正确行为不能退化;边界案例集 = 过度转接应改善)")
print("=" * 74)
header = f"{'系统提示词版本':<26}{'保留任务集(holdout)':<20}{'边界案例集(boundary)':<20}"
print(header)
print("-" * 74)
for label, holdout, boundary in rows:
print(f"{label:<24}{_pct(holdout):<22}{_pct(boundary):<22}")
print("=" * 74)
def _select_cases(limit_per_group=None, groups=GROUPS):
"""按分组筛选用例,并对每组最多取 limit_per_group 个(None 表示不限制)。"""
picked, counts = [], {}
for c in CASES:
g = c["group"]
if g not in groups:
continue
if limit_per_group and counts.get(g, 0) >= limit_per_group:
continue
picked.append(c)
counts[g] = counts.get(g, 0) + 1
return picked
def main(cases=None, rounds=3, output=None):
if cases is None:
cases = CASES
reset_api_turns()
campaign_started = time.time()
print("#" * 74)
print("# 实验 9-3:基于失败轨迹的系统提示词自动优化(航空客服场景)")
print(f"# LLM 提供商: {get_provider()} 模型: {get_model()}")
print(f"# 用例数: {len(cases)}(保留集 + 边界集) Coding Agent 优化轮数上限: {rounds}")
print("#" * 74)
# ---- 准备:把初始 prompt 复制成本次运行的工作副本(Coding Agent 会改写它)----
os.makedirs(os.path.dirname(WORKING_PROMPT), exist_ok=True)
shutil.copyfile(INITIAL_PROMPT, WORKING_PROMPT)
# ---- 步骤 1:评测初始 prompt ----
print("\n【步骤 1】用初始系统提示词评测(观察是否过度转接)")
before = evaluate_prompt(_read(INITIAL_PROMPT), label="初始 prompt", cases=cases)
print(
f"\n 初始结果:保留集 {_pct(before['holdout'])}"
f"边界集 {_pct(before['boundary'])}"
)
over_transfer = [
r for r in before["results"]
if r["group"] == "boundary" and not r["should_transfer"] and r["transferred"]
]
print(f" 边界案例中出现【过度转接】的用例数:{len(over_transfer)} / "
f"{len([r for r in before['results'] if r['group'] == 'boundary'])}")
for r in over_transfer:
print(f" - {r['id']}:政策争议却直接转人工,原因『{r['transfer_reason']}")
# ---- 步骤 2:由失败轨迹形成学习信号 ----
learning_signal = diagnose_failures(before)
print("\n【步骤 2】将失败轨迹整理为三维诊断")
print(format_learning_signal(learning_signal))
# ---- 步骤 3Coding Agent 生成候选 prompt ----
print("\n【步骤 3】Coding Agent 读取诊断并生成候选系统提示词……")
candidate_started = time.time()
opt = optimize_prompt(WORKING_PROMPT, learning_signal, max_rounds=rounds, verbose=True)
failure_to_candidate_seconds = time.time() - candidate_started
manifest = build_candidate_manifest(opt, learning_signal)
print(f"\n Coding Agent 改动说明:{opt['rationale']}")
print("\n ---------- 系统提示词文件 diff(真实写入磁盘)----------")
print(opt["diff"] if opt["diff"].strip() else " (无改动)")
print(" --------------------------------------------------------")
print(f" 候选补丁来源:{', '.join(manifest['source_case_ids'])}")
print(f" 候选补丁作用域:{manifest['scope']}")
# ---- 步骤 4:评测候选 prompt 并运行发布门槛 ----
print("\n【步骤 4】评测候选系统提示词并运行发布门槛")
after = evaluate_prompt(opt["after"], label="自动优化后 prompt", cases=cases)
gate = evaluate_release_gate(before, after, manifest)
print(f" 发布决定:{gate['decision']}")
for check, passed in gate["checks"].items():
print(f" {'' if passed else ''} {check}")
# ---- 步骤 5:对照人工调优版 ----
print("\n【步骤 5】对照组:人工调优版系统提示词")
manual = evaluate_prompt(_read(MANUAL_PROMPT), label="人工调优版 prompt(对照)", cases=cases)
# ---- 步骤 6:对比表 ----
print_table([
("初始 prompt(优化前)", before["holdout"], before["boundary"]),
("自动优化后 prompt", after["holdout"], after["boundary"]),
("人工调优版(对照)", manual["holdout"], manual["boundary"]),
])
# ---- 结论 ----
b_before_c, b_before_n = before["boundary"]
b_after_c, _ = after["boundary"]
h_before_c, _ = before["holdout"]
h_after_c, _ = after["holdout"]
print("\n【结论】")
print(f" · 边界案例集正确率:{b_before_c}/{b_before_n}{b_after_c}/{b_before_n} "
f"{'提升 ✓' if b_after_c > b_before_c else '未提升'}")
print(f" · 保留任务集正确率:{h_before_c}{h_after_c} "
f"{'未退化 ✓' if h_after_c >= h_before_c else '退化 ✗'}")
print(f"\n 候选工作副本已写入:{WORKING_PROMPT}")
print(" 它不会覆盖稳定版本;只有 release_to_canary 才允许进入灰度。")
# ---- 可选:把对比结果落盘为 JSON,便于复现与二次分析 ----
before_by_id = {row["id"]: row for row in before["results"]}
after_by_id = {row["id"]: row for row in after["results"]}
regressions = [
identifier for identifier, old in before_by_id.items()
if old["correct"] and not after_by_id[identifier]["correct"]
]
boundary_fixed = [
identifier for identifier, old in before_by_id.items()
if old["group"] == "boundary" and not old["correct"] and after_by_id[identifier]["correct"]
]
api_turns = get_api_turns()
gates = [
{"name": "full_holdout_and_boundary_sets_run", "passed": len(cases) == len(CASES) and {c["group"] for c in cases} == {"holdout", "boundary"}, "evidence": {"selected": len(cases), "canonical": len(CASES)}},
{"name": "same_model_and_same_cases_for_three_controls", "passed": all({r["id"] for r in report["results"]} == {c["id"] for c in cases} for report in (before, after, manual)), "evidence": get_model()},
{"name": "real_task_agent_calls", "passed": any(turn["kind"].startswith("task_agent") for turn in api_turns), "evidence": sum(turn["kind"].startswith("task_agent") for turn in api_turns)},
{"name": "real_llm_judge_calls", "passed": any(turn["kind"] == "llm_judge" for turn in api_turns), "evidence": sum(turn["kind"] == "llm_judge" for turn in api_turns)},
{"name": "real_coding_agent_call", "passed": any(turn["kind"] == "coding_agent" for turn in api_turns), "evidence": sum(turn["kind"] == "coding_agent" for turn in api_turns)},
{"name": "learning_signal_has_three_dimensions_and_source_ids", "passed": set(learning_signal["dimensions"]) == {"rule_compliance", "task_resolution", "compliant_flexibility"} and bool(learning_signal["source_case_ids"]), "evidence": learning_signal["source_case_ids"]},
{"name": "minimal_old_to_new_patch_is_auditable", "passed": bool(manifest.get("edits")) and bool(manifest.get("diff")), "evidence": manifest.get("edits")},
{"name": "release_gate_evaluated_all_four_manuscript_conditions", "passed": set(gate["checks"]) >= {"patch_is_nonempty", "patch_is_auditable_old_to_new_edit", "source_cases_are_recorded", "holdout_did_not_regress", "boundary_improved"}, "evidence": gate["checks"]},
{"name": "stable_prompt_not_overwritten", "passed": _read(INITIAL_PROMPT) == opt["before"], "evidence": {"stable": INITIAL_PROMPT, "candidate": WORKING_PROMPT}},
{"name": "raw_credential_free_api_receipts_saved", "passed": bool(api_turns), "evidence": len(api_turns)},
]
execution_accepted = all(item["passed"] for item in gates)
result_claims = {
"boundary_improved": after["boundary"][0] > before["boundary"][0],
"holdout_not_degraded": after["holdout"][0] >= before["holdout"][0],
"automatic_candidate_released_only_to_canary": gate["decision"] == "release_to_canary",
"automatic_candidate_compared_with_manual": True,
}
summary = {
"schema_version": 2,
"experiment_id": "9-3",
"canonical_source": "book/chapter9.md#实验-9-3-基于失败轨迹优化系统提示词",
"evidence_mode": "real_task_agent_llm_judge_coding_agent_full_campaign",
"created_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat(),
"provider": get_provider(),
"model": get_model(),
"backend": get_backend_metadata(),
"credential_value_recorded": False,
"rounds": rounds,
"num_cases": len(cases),
"case_ids": [case["id"] for case in cases],
"learning_signal": learning_signal,
"candidate_manifest": manifest,
"release_gate": gate,
"rationale": opt["rationale"],
"diff": opt["diff"],
"prompt_metrics": {
"initial_characters": len(opt["before"]),
"candidate_characters": len(opt["after"]),
"growth_characters": len(opt["after"]) - len(opt["before"]),
"manual_characters": len(_read(MANUAL_PROMPT)),
"introduced_regressions": len(regressions),
"regression_case_ids": regressions,
"boundary_failures_fixed": len(boundary_fixed),
"boundary_fixed_case_ids": boundary_fixed,
"failure_to_candidate_seconds": round(failure_to_candidate_seconds, 6),
"campaign_elapsed_seconds": round(time.time() - campaign_started, 6),
},
"evaluations": {"initial": before, "automatic_candidate": after, "manual": manual},
"rows": [
{"label": "初始 prompt(优化前)", "holdout": list(before["holdout"]),
"boundary": list(before["boundary"])},
{"label": "自动优化后 prompt", "holdout": list(after["holdout"]),
"boundary": list(after["boundary"])},
{"label": "人工调优版(对照)", "holdout": list(manual["holdout"]),
"boundary": list(manual["boundary"])},
],
"usage": usage_summary(),
"api_turns": api_turns,
"acceptance": {
"gates": gates,
"execution_accepted": execution_accepted,
"result_claims": result_claims,
"all_manuscript_result_claims_observed": all(result_claims.values()),
},
}
if output:
os.makedirs(os.path.dirname(os.path.abspath(output)), exist_ok=True)
with open(output, "w", encoding="utf-8") as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print(f" 对比结果已写入:{output}")
return summary
def _build_parser():
parser = argparse.ArgumentParser(
prog="demo.py",
description="实验 9-3:从失败轨迹诊断到候选补丁与发布门槛(航空客服场景)。",
formatter_class=argparse.RawTextHelpFormatter,
epilog=(
"示例:\n"
" python demo.py # 完整运行:10 个用例 × 3 份 prompt\n"
" python demo.py --quick # 每组只取 2 个用例,省时省钱\n"
" python demo.py --group boundary # 只评测边界案例集\n"
" python demo.py --rounds 5 --model gpt-5.6-luna\n"
" python demo.py --output output/run.json # 把对比结果写成 JSON\n"
" python demo.py --dry-run # 离线:只打印配置与用例数,不调用 API"
),
)
parser.add_argument(
"--quick", action="store_true",
help="快速演示模式:每组只取 2 个用例,减少 API 调用与耗时。",
)
parser.add_argument(
"--limit", type=int, default=None, metavar="N",
help="每组最多评测 N 个用例(覆盖 --quick)。",
)
parser.add_argument(
"--group", choices=("holdout", "boundary", "both"), default="both",
help="选择评测的任务集:holdout(保留集) / boundary(边界集) / both(默认,两者都跑)。",
)
parser.add_argument(
"--rounds", type=int, default=3, metavar="N",
help="Coding Agent 自动改写提示词的最大重试轮数(默认 3)。",
)
parser.add_argument(
"--model", default=None, metavar="NAME",
help="覆盖 LLM 模型名(等价于设置环境变量 LLM_MODEL,如 gpt-5.6-luna)。",
)
parser.add_argument(
"--provider", choices=("openai", "moonshot", "ark", "openrouter"), default=None,
help="覆盖 LLM 提供商(等价于设置环境变量 LLM_PROVIDER,默认 openai)。",
)
parser.add_argument(
"--output", default=None, metavar="PATH",
help="把优化前后 + 人工对照的对比结果写入指定 JSON 文件(如 output/run.json)。",
)
parser.add_argument(
"--dry-run", action="store_true",
help="离线自检:只打印解析后的配置与选中用例数,不调用任何 LLM API。",
)
return parser
if __name__ == "__main__":
args = _build_parser().parse_args()
# 命令行覆盖优先级高于环境变量:get_provider()/get_model() 均在调用时读取环境变量
if args.provider:
os.environ["LLM_PROVIDER"] = args.provider
if args.model:
os.environ["LLM_MODEL"] = args.model
limit = args.limit if args.limit is not None else (2 if args.quick else None)
groups = GROUPS if args.group == "both" else (args.group,)
cases = _select_cases(limit, groups=groups)
if args.dry_run:
# 离线路径:不触发任何网络请求,仅用于验证参数解析与用例选择
print("[dry-run] 解析后的运行配置(不调用 API):")
print(f" LLM 提供商 : {get_provider()}")
print(f" LLM 模型 : {get_model()}")
print(f" 优化轮数 : {args.rounds}")
print(f" 任务集 : {args.group}")
print(f" 选中用例数 : {len(cases)} -> {[c['id'] for c in cases]}")
print(f" 输出文件 : {args.output or '(不写文件)'}")
sys.exit(0)
try:
main(cases=cases, rounds=args.rounds, output=args.output)
except RuntimeError as e:
# 例如 API Key 未设置:给出清晰的人类可读错误,而非原始 traceback
print(f"\n[错误] {e}", file=sys.stderr)
sys.exit(1)