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
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:
@@ -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))
|
||||
|
||||
# ---- 步骤 3:Coding 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)
|
||||
Reference in New Issue
Block a user