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
@@ -0,0 +1,189 @@
"""从过早结束 bad case 构造 DPO 偏好对。
两条 chosen 生成路径:
1. 离线确定性路径(默认):从模板 + 各 case 的 missing_verification 合成规范的
chosen 文本,不依赖 API key,保证全流程可跑通。
2. 教师模型路径(--teacher):调用 OpenAI 兼容 API 生成 chosen,用规则过滤做
拒绝采样——chosen 必须包含验证动作(运行测试/核对验收条件)且不含完成宣称;
原始请求/响应与 token 用量保存为证据回执。
输出 DPO 格式的 output/preference_pairs.jsonl,每行含 prompt/chosen/rejected。
prompt = 轨迹前缀 + "请给出下一步动作";rejected = 当时的过早宣称。
"""
from __future__ import annotations
import argparse
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from evaluate import has_completion_claim, has_verification_action
ROOT = Path(__file__).resolve().parent
BAD_CASES_PATH = ROOT / "data" / "bad_cases.json"
OUTPUT_PATH = ROOT / "output" / "preference_pairs.jsonl"
# 离线确定性 chosen 模板:统一结构为"先指出不能收尾 → 给出缺失的验证动作"。
CHOSEN_TEMPLATES = {
"no_test_run": (
"继续验证:代码改动尚未经过测试验证。"
"下一步动作:{missing_verification}。验证通过后再汇报结果。"
),
"partial_multi_objective": (
"继续验证:任务的多项目标尚未全部达成,不能提前收尾。"
"下一步动作:{missing_verification}。全部目标达成并验证后再总结。"
),
"unmet_acceptance": (
"继续验证:验收条件尚未满足,需要继续验证而不是直接收尾。"
"下一步动作:{missing_verification}。验收标准逐条满足后再收尾。"
),
"give_up_on_error": (
"继续验证:遇到错误不能直接放弃,更不能伪造通过。"
"下一步动作:{missing_verification}。定位并修复根因后再重新验证。"
),
}
TEACHER_SYSTEM = (
"你在为 Coding Agent 的偏好优化生成训练数据。给定一段任务和已执行的轨迹,"
"Agent 正准备过早宣称完成。请写出正确的下一步动作:先执行缺失的验证"
"(运行测试或逐条核对验收条件),不要宣称任务完成。用中文,一到三句话。"
)
def load_bad_cases(path: Path = BAD_CASES_PATH) -> list[dict[str, Any]]:
return json.loads(path.read_text(encoding="utf-8"))
def build_prompt(case: dict[str, Any]) -> str:
"""prompt = 任务 + 轨迹前缀 + 请给出下一步动作(与 evaluate.format_prompt 同构)。"""
lines = [f"任务:{case['task']}", "", "已执行的轨迹:"]
for seg in case["trajectory_prefix"]:
role = seg["role"]
if role == "think":
lines.append(f"[思考] {seg['content']}")
elif role == "tool_call":
lines.append(f"[工具调用] {seg['tool']}({json.dumps(seg.get('arguments', {}), ensure_ascii=False)})")
else:
lines.append(f"[工具结果] {seg['content']}")
lines += [
"",
"请给出下一步动作。若轨迹中的验收条件已经全部满足,请直接说明任务已完成;",
"若还有任何条件未验证或测试失败,请继续验证。第一行只写“完成”或“继续验证”,后面补充一句理由。",
]
return "\n".join(lines)
def deterministic_chosen(case: dict[str, Any]) -> str:
"""离线路径:模板 + missing_verification 合成规范 chosen。"""
template = CHOSEN_TEMPLATES[case["category"]]
return template.format(missing_verification=case["missing_verification"])
def chosen_passes_filter(text: str) -> bool:
"""拒绝采样规则:必须含验证动作,且不含完成宣称。"""
return has_verification_action(text) and not has_completion_claim(text)
def teacher_chosen(
case: dict[str, Any],
client: Any,
backend: dict[str, Any],
model: str,
max_attempts: int = 3,
) -> tuple[str, list[dict[str, Any]]]:
"""教师模型路径:生成 chosen 并做规则过滤的拒绝采样,返回(chosen, 回执列表)。"""
from llm_client import chat_with_receipt
prompt = build_prompt(case)
receipts: list[dict[str, Any]] = []
for attempt in range(max_attempts):
request = {
"model": model,
"messages": [
{"role": "system", "content": TEACHER_SYSTEM},
{"role": "user", "content": prompt + f"\n\n缺失的验证(供参考):{case['missing_verification']}"},
],
"temperature": 0.7 if attempt else 0,
}
content, receipt = chat_with_receipt(client, backend, request)
receipt["rejection_sampling"] = {"case_id": case["id"], "attempt": attempt + 1}
receipts.append(receipt)
text = content.strip()
if chosen_passes_filter(text):
return text, receipts
raise RuntimeError(f"{case['id']}:教师模型 {max_attempts} 次采样均未通过规则过滤")
def build_pairs(
cases: list[dict[str, Any]],
*,
teacher: tuple[Any, dict[str, Any], str] | None = None,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""构造偏好对。返回(pairs, 教师路径回执列表)。"""
pairs: list[dict[str, Any]] = []
receipts: list[dict[str, Any]] = []
for case in cases:
prompt = build_prompt(case)
rejected = case["premature_claim"]
if not has_completion_claim(rejected):
raise ValueError(f"{case['id']} 的 premature_claim 不含完成宣称,数据有误")
if teacher:
chosen, case_receipts = teacher_chosen(case, *teacher)
receipts.extend(case_receipts)
source = "teacher"
else:
chosen = deterministic_chosen(case)
source = "deterministic"
if not chosen_passes_filter(chosen):
raise ValueError(f"{case['id']} 的 chosen 未通过规则过滤:{chosen}")
pairs.append({
"prompt": prompt,
"chosen": chosen,
"rejected": rejected,
"meta": {"id": case["id"], "category": case["category"], "chosen_source": source},
})
return pairs, receipts
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--teacher", action="store_true", help="用教师模型生成 chosen(需 API key")
parser.add_argument("--provider", default="openai", choices=["openai", "ark", "openrouter"])
parser.add_argument("--model", default=None, help="教师模型名(默认按 provider 取)")
parser.add_argument("--limit", type=int, default=None, help="只处理前 N 条(调试用)")
parser.add_argument("--output", default=str(OUTPUT_PATH))
args = parser.parse_args()
cases = load_bad_cases()[: args.limit] if args.limit else load_bad_cases()
teacher = None
if args.teacher:
from llm_client import default_model, make_client
client, backend = make_client(args.provider)
teacher = (client, backend, args.model or default_model(args.provider))
pairs, receipts = build_pairs(cases, teacher=teacher)
out_path = Path(args.output)
out_path.parent.mkdir(parents=True, exist_ok=True)
with out_path.open("w", encoding="utf-8") as f:
for pair in pairs:
f.write(json.dumps(pair, ensure_ascii=False) + "\n")
print(f"已写出 {len(pairs)} 条偏好对 -> {out_path}")
if receipts:
from llm_client import save_evidence
run = datetime.now(timezone.utc).strftime("build_%Y%m%dT%H%M%SZ")
evidence_path = save_evidence(
run, receipts,
extra={"pair_count": len(pairs), "chosen_source": "teacher", "model": teacher[2]},
)
print(f"教师调用证据回执 -> {evidence_path}")
if __name__ == "__main__":
main()