Files
ai-agent-book/chapter9/gaia-experience/experience_documents.py
T
liqiang b119135836
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
ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
2026-08-20 13:12:50 +00:00

211 lines
8.1 KiB
Python

"""Cross-trajectory Markdown experience documents for Experiment 9-2.
This module is intentionally independent of AWorld so that the learning
method can be inspected and tested offline. AWorld trajectories only need to
be converted to the small evaluated record schema used here.
"""
from __future__ import annotations
from collections import Counter, defaultdict
from dataclasses import dataclass
from datetime import date
from pathlib import Path
import re
from typing import Any, Dict, Iterable, List, Sequence
def outcome_label(score: float) -> str:
if score >= 0.9:
return "success"
if score >= 0.4:
return "partial"
return "failure"
def _slug(text: str) -> str:
value = re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
return value or "experience"
def _tokens(text: str) -> set[str]:
return set(re.findall(r"[a-z0-9_]+|[\u4e00-\u9fff]", text.lower()))
@dataclass(frozen=True)
class ExperienceDocument:
task_family: str
capabilities: tuple[str, ...]
applies_when: tuple[str, ...]
recommended_strategies: tuple[str, ...]
common_pitfalls: tuple[str, ...]
exceptions: tuple[str, ...]
sources: tuple[str, ...]
last_validated: str
@property
def document_id(self) -> str:
return _slug(self.task_family)
def to_markdown(self) -> str:
def bullets(items: Sequence[str]) -> str:
return "\n".join(f"- {item}" for item in items) if items else "- 暂无可靠结论"
return (
f"# {self.task_family}:可复用经验\n\n"
f"最近验证时间:{self.last_validated}\n\n"
"## 适用场景\n\n"
f"{bullets(self.applies_when)}\n\n"
"## 推荐策略\n\n"
f"{bullets(self.recommended_strategies)}\n\n"
"## 常见误区\n\n"
f"{bullets(self.common_pitfalls)}\n\n"
"## 例外条件\n\n"
f"{bullets(self.exceptions)}\n\n"
"## 来源轨迹\n\n"
f"{bullets(self.sources)}\n"
)
def build_documents(
trajectories: Iterable[Dict[str, Any]], *, validated_on: str | None = None
) -> List[ExperienceDocument]:
"""Compare evaluated trajectories and create one document per task family.
A strategy must be supported by at least two non-failed trajectories. A
single lucky run is retained as evidence but not promoted to a reusable
recommendation.
"""
groups: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
for item in trajectories:
record = dict(item)
record["outcome"] = outcome_label(float(record.get("environment_score", 0.0)))
groups[record["task_family"]].append(record)
documents: List[ExperienceDocument] = []
for family, records in sorted(groups.items()):
strategy_support: Counter[str] = Counter()
pitfall_support: Counter[str] = Counter()
capabilities: set[str] = set()
applies_when: set[str] = set()
exceptions: set[str] = set()
for record in records:
capabilities.update(record.get("capabilities", []))
applies_when.update(record.get("applies_when", []))
exceptions.update(record.get("exceptions", []))
if record["outcome"] != "failure":
strategy_support.update(set(record.get("observed_strategies", [])))
if record["outcome"] != "success":
pitfall_support.update(set(record.get("mistakes", [])))
recommendations = tuple(
text for text, count in strategy_support.most_common() if count >= 2
)
pitfalls = tuple(text for text, _ in pitfall_support.most_common())
sources = tuple(
f"{record['id']} ({record['outcome']}, score={float(record.get('environment_score', 0.0)):.2f})"
for record in records
)
documents.append(ExperienceDocument(
task_family=family,
capabilities=tuple(sorted(capabilities)),
applies_when=tuple(sorted(applies_when)) or (f"任务属于 {family}",),
recommended_strategies=recommendations,
common_pitfalls=pitfalls,
exceptions=tuple(sorted(exceptions)),
sources=sources,
last_validated=validated_on or date.today().isoformat(),
))
return documents
def write_documents(documents: Iterable[ExperienceDocument], output_dir: Path) -> List[Path]:
output_dir.mkdir(parents=True, exist_ok=True)
paths = []
for document in documents:
path = output_dir / f"{document.document_id}.md"
path.write_text(document.to_markdown(), encoding="utf-8")
paths.append(path)
return paths
def retrieve_documents(
query: str, task_family: str, documents: Sequence[ExperienceDocument], top_k: int = 1
) -> List[ExperienceDocument]:
query_tokens = _tokens(query)
def score(document: ExperienceDocument) -> tuple[float, str]:
searchable = " ".join((
document.task_family,
*document.capabilities,
*document.applies_when,
*document.recommended_strategies,
))
overlap = len(query_tokens & _tokens(searchable))
family_bonus = 10.0 if document.task_family == task_family else 0.0
return family_bonus + overlap, document.document_id
return sorted(documents, key=score, reverse=True)[:top_k]
def _trajectory_summary(record: Dict[str, Any]) -> Dict[str, Any]:
"""The direct-single-trajectory baseline used in the experiment."""
return {
"id": record["id"],
"task_family": record["task_family"],
"text": f"{record.get('question', '')} {' '.join(record.get('observed_strategies', []))}",
# Direct retrieval has no cross-run check: even a failed path may be
# presented to the next agent as the action taken in that case.
"directives": list(record.get("observed_strategies", [])),
}
def _retrieve_summary(query: str, family: str, records: Sequence[Dict[str, Any]]) -> Dict[str, Any]:
query_tokens = _tokens(query)
summaries = [_trajectory_summary(record) for record in records]
def score(summary: Dict[str, Any]) -> tuple[float, str]:
family_bonus = 10.0 if summary["task_family"] == family else 0.0
return family_bonus + len(query_tokens & _tokens(summary["text"])), summary["id"]
return max(summaries, key=score)
def evaluate_retrieval_baselines(
trajectories: Sequence[Dict[str, Any]],
documents: Sequence[ExperienceDocument],
transfer_cases: Sequence[Dict[str, Any]],
) -> Dict[str, Dict[str, float]]:
"""Measure transfer, context cost and negative transfer for three modes."""
aggregates: Dict[str, Dict[str, float]] = {}
for mode in ("no_experience", "single_trajectory_summary", "knowledge_document"):
successes = negatives = retrieved_chars = 0
for case in transfer_cases:
if mode == "no_experience":
directives: List[str] = []
content = ""
elif mode == "single_trajectory_summary":
summary = _retrieve_summary(case["query"], case["task_family"], trajectories)
directives = summary["directives"]
content = summary["text"]
else:
document = retrieve_documents(case["query"], case["task_family"], documents)[0]
directives = list(document.recommended_strategies)
content = document.to_markdown()
guidance = " ".join(directives).lower()
expected = [term.lower() for term in case.get("expected_guidance", [])]
harmful = [term.lower() for term in case.get("harmful_guidance", [])]
successes += int(all(term in guidance for term in expected))
negatives += int(any(term in guidance for term in harmful))
retrieved_chars += len(content)
count = max(1, len(transfer_cases))
aggregates[mode] = {
"transfer_success_rate": round(successes / count, 3),
"negative_transfer_rate": round(negatives / count, 3),
"average_retrieved_characters": round(retrieved_chars / count, 1),
}
return aggregates