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,379 @@
"""Trajectory consistency checker for chapter 8.
Detects ungrounded claims, contradictions, hallucinated tool results, and
unsupported conclusions in agent trajectories. Purely heuristic and
deterministic: no network calls, no LLM dependency.
A trajectory is a list of step dictionaries. Each step may contain:
step_id int -- ordinal identifier (defaults to list index)
action str -- what the agent did in this step
claims list[str] -- assertions made by the agent
tool_result Any -- the actual result returned by a tool
claimed_tool_result Any -- what the agent says the tool returned
observation str -- a textual observation the agent received
final_answer str -- the agent's final conclusion (last step)
The checker scores four dimensions:
claim_grounding -- fraction of claims backed by prior evidence
contradiction_freedom -- 1 minus the contradiction rate
evidence_chain_integrity -- fraction of tool results reported faithfully
conclusion_support -- whether the final answer follows from evidence
"""
from __future__ import annotations
import json
import re
from dataclasses import dataclass
from typing import Any
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
VIOLATION_UNGROUNDED = "ungrounded_claim"
VIOLATION_CONTRADICTION = "contradiction"
VIOLATION_HALLUCINATED = "hallucinated_result"
VIOLATION_UNSUPPORTED = "unsupported_conclusion"
DIMENSIONS = (
"claim_grounding",
"contradiction_freedom",
"evidence_chain_integrity",
"conclusion_support",
)
# Tokens that flip a claim's polarity.
_NEGATION_TOKENS = frozenset({
"not", "no", "never", "none", "nobody", "nothing", "neither",
"nor", "cannot", "cant", "wont", "dont", "doesnt", "didnt",
"isnt", "wasnt", "arent", "werent", "hasnt", "havent", "hadnt",
"wouldnt", "couldnt", "shouldnt",
})
# Stopwords excluded when computing token overlap for grounding.
_STOPWORDS = frozenset({
"the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
"to", "of", "in", "on", "at", "by", "for", "with", "about", "as",
"into", "from", "that", "this", "these", "those", "it", "its",
"has", "have", "had", "do", "does", "did", "will", "would", "can",
"could", "should", "shall", "may", "might", "must", "and", "or",
"but", "if", "then", "so", "than", "too", "very", "just", "also",
"i", "we", "you", "they", "he", "she", "my", "our", "your",
"been", "being", "am",
})
# Minimum significant-token overlap ratio for a claim to be considered grounded.
_GROUNDING_THRESHOLD = 0.5
# Minimum Jaccard similarity between claim cores for a contradiction check.
_CONTRADICTION_SIMILARITY = 0.5
# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------
@dataclass
class ConsistencyViolation:
"""A single consistency violation found in a trajectory step."""
step_id: int
violation_type: str # ungrounded_claim, contradiction, hallucinated_result, unsupported_conclusion
description: str
evidence: dict[str, Any]
@dataclass
class ConsistencyReport:
"""Structured report returned by ``check_trajectory``."""
total_steps: int
total_claims: int
violations: list[ConsistencyViolation]
dimension_scores: dict[str, float]
overall_consistency_score: float
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _tokenize(text: str) -> list[str]:
"""Lowercase alphanumeric tokens of length > 1."""
return [t for t in re.findall(r"[a-z0-9]+", text.lower()) if len(t) > 1]
def _significant_tokens(text: str) -> set[str]:
"""Tokens that carry semantic weight (stopwords and negations removed)."""
return {
t
for t in _tokenize(text)
if t not in _STOPWORDS and t not in _NEGATION_TOKENS
}
def _serialize_evidence(value: Any) -> str:
"""Convert a tool result or observation into a comparable string."""
if value is None:
return ""
if isinstance(value, str):
return value
try:
return json.dumps(value, sort_keys=True)
except (TypeError, ValueError):
return str(value)
def _step_id(step: dict[str, Any], idx: int) -> int:
sid = step.get("step_id", idx)
if isinstance(sid, bool) or not isinstance(sid, int):
return idx
return sid
# ---------------------------------------------------------------------------
# Checker
# ---------------------------------------------------------------------------
class TrajectoryConsistencyChecker:
"""Check agent trajectories for internal consistency."""
def __init__(self) -> None:
self.grounding_threshold: float = _GROUNDING_THRESHOLD
self.contradiction_similarity: float = _CONTRADICTION_SIMILARITY
# -- public API ---------------------------------------------------------
def check_trajectory(self, trajectory: list[dict[str, Any]]) -> ConsistencyReport:
"""Run all consistency checks and return a structured report."""
if not trajectory:
return ConsistencyReport(
total_steps=0,
total_claims=0,
violations=[],
dimension_scores={d: 1.0 for d in DIMENSIONS},
overall_consistency_score=1.0,
)
violations: list[ConsistencyViolation] = []
total_claims = 0
grounded_claims = 0
all_claims: list[tuple[int, str]] = []
evidence_strings: list[str] = []
steps_with_tool_results = 0
steps_with_valid_results = 0
final_answer: str | None = None
final_step_id: int | None = None
for idx, step in enumerate(trajectory):
sid = _step_id(step, idx)
# --- accumulate evidence from this step ------------------------
observation = step.get("observation")
if isinstance(observation, str) and observation.strip():
evidence_strings.append(observation)
tool_result = step.get("tool_result")
if tool_result is not None:
serialized = _serialize_evidence(tool_result)
if serialized:
evidence_strings.append(serialized)
steps_with_tool_results += 1
claimed = step.get("claimed_tool_result")
if claimed is not None:
if tool_result != claimed:
violations.append(ConsistencyViolation(
step_id=sid,
violation_type=VIOLATION_HALLUCINATED,
description=(
f"Step {sid} claims a tool result that differs "
f"from the actual result"
),
evidence={
"actual_result": tool_result,
"claimed_result": claimed,
},
))
else:
steps_with_valid_results += 1
else:
steps_with_valid_results += 1
# --- check claim grounding -------------------------------------
claims = step.get("claims", [])
if not isinstance(claims, list):
claims = []
for claim in claims:
if not isinstance(claim, str) or not claim.strip():
continue
total_claims += 1
all_claims.append((sid, claim))
if self.check_claim_grounded(claim, list(evidence_strings)):
grounded_claims += 1
else:
violations.append(ConsistencyViolation(
step_id=sid,
violation_type=VIOLATION_UNGROUNDED,
description=(
f"Claim at step {sid} is not grounded in prior "
f"evidence: {claim}"
),
evidence={
"claim": claim,
"available_evidence": list(evidence_strings),
},
))
# --- track final answer ---------------------------------------
fa = step.get("final_answer")
if isinstance(fa, str) and fa.strip():
final_answer = fa
final_step_id = sid
# --- contradictions ------------------------------------------------
contradiction_violations = self.find_contradictions(all_claims)
violations.extend(contradiction_violations)
# --- unsupported conclusion ---------------------------------------
conclusion_supported = True
if final_answer is not None and final_step_id is not None:
conclusion_evidence = evidence_strings + [text for _, text in all_claims]
if not self.check_claim_grounded(final_answer, conclusion_evidence):
conclusion_supported = False
violations.append(ConsistencyViolation(
step_id=final_step_id,
violation_type=VIOLATION_UNSUPPORTED,
description=(
f"Final answer at step {final_step_id} is not supported "
f"by the evidence chain"
),
evidence={
"final_answer": final_answer,
"available_evidence": conclusion_evidence,
},
))
# --- dimension scores ---------------------------------------------
claim_grounding = grounded_claims / total_claims if total_claims else 1.0
contradiction_freedom = (
max(0.0, 1.0 - len(contradiction_violations) / total_claims)
if total_claims
else 1.0
)
evidence_chain_integrity = (
steps_with_valid_results / steps_with_tool_results
if steps_with_tool_results
else 1.0
)
conclusion_support = 1.0 if conclusion_supported else 0.0
dimension_scores = {
"claim_grounding": round(claim_grounding, 4),
"contradiction_freedom": round(contradiction_freedom, 4),
"evidence_chain_integrity": round(evidence_chain_integrity, 4),
"conclusion_support": round(conclusion_support, 4),
}
overall = round(sum(dimension_scores.values()) / len(dimension_scores), 4)
return ConsistencyReport(
total_steps=len(trajectory),
total_claims=total_claims,
violations=violations,
dimension_scores=dimension_scores,
overall_consistency_score=overall,
)
def check_claim_grounded(self, claim: str, available_evidence: list[str]) -> bool:
"""Return ``True`` if *claim* is backed by any evidence string."""
claim_tokens = _significant_tokens(claim)
if not claim_tokens:
return True
for evidence in available_evidence:
ev_tokens = _significant_tokens(evidence)
if not ev_tokens:
continue
overlap = len(claim_tokens & ev_tokens) / len(claim_tokens)
if overlap >= self.grounding_threshold:
return True
return False
def find_contradictions(
self, claims: list[tuple[int, str]]
) -> list[ConsistencyViolation]:
"""Detect contradictions between claims across steps.
Two contradiction patterns are recognised:
* **polarity** -- one claim affirms X, a later claim denies X.
* **numeric** -- two claims share the same subject but cite
disjoint numeric values.
"""
violations: list[ConsistencyViolation] = []
parsed: list[tuple[int, str, bool, set[str], set[str]]] = []
for step_id, text in claims:
tokens = _tokenize(text)
negated = any(t in _NEGATION_TOKENS for t in tokens)
core = _significant_tokens(text)
numbers = set(re.findall(r"\d+", text.lower()))
parsed.append((step_id, text, negated, core, numbers))
for i in range(len(parsed)):
sid_a, text_a, neg_a, core_a, nums_a = parsed[i]
if not core_a:
continue
for j in range(i + 1, len(parsed)):
sid_b, text_b, neg_b, core_b, nums_b = parsed[j]
if sid_b <= sid_a:
continue
if not core_b:
continue
jaccard = len(core_a & core_b) / len(core_a | core_b)
if jaccard < self.contradiction_similarity:
continue
if neg_a != neg_b:
violations.append(ConsistencyViolation(
step_id=sid_b,
violation_type=VIOLATION_CONTRADICTION,
description=(
f"Claim at step {sid_b} contradicts claim at "
f"step {sid_a}"
),
evidence={
"earlier_step": sid_a,
"earlier_claim": text_a,
"later_step": sid_b,
"later_claim": text_b,
"contradiction_type": "polarity",
},
))
elif nums_a and nums_b and nums_a.isdisjoint(nums_b):
violations.append(ConsistencyViolation(
step_id=sid_b,
violation_type=VIOLATION_CONTRADICTION,
description=(
f"Claim at step {sid_b} contradicts numeric value "
f"in claim at step {sid_a}"
),
evidence={
"earlier_step": sid_a,
"earlier_claim": text_a,
"later_step": sid_b,
"later_claim": text_b,
"contradiction_type": "numeric",
"earlier_numbers": sorted(nums_a),
"later_numbers": sorted(nums_b),
},
))
return violations