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,195 @@
"""
离线后端:让整条流水线在**没有 OpenAI key** 时也能跑通,用于验证机制、
量化 token/延迟,并让读者零成本复现"三种策略"的对比结构。
包含两部分:
1) LocalEmbedder —— 本地哈希词袋嵌入(中文字 unigram/bigram + 英文词),
无需联网即可支撑 discover_tools / 检索预筛选的语义相似度。
2) MockChatClient —— 一个确定性的"脚本化"模型,接口与 OpenAI 客户端一致
client.chat.completions.create(...).choices[0].message.content)。
它按关键词把任务拆成若干子任务,遵循 ReAct 文本协议逐步调用工具。
重要边界说明:
- MockChatClient 是一个**强启发式路由器**,不代表真实小模型的能力,因此它**不会**复现
书中"超长上下文下指令遵循退化、错选通用工具"的现象——那需要真实的小参数量模型。
- 离线模式下真实可复现的是:① 各策略注入的 token 量(tiktoken 真实计算);
② 检索预筛选"一次性匹配"的结构性局限(若第二个子任务的专用工具没被初始检索选中,
模型就调用不到它 → 该子任务失败);③ 主动发现按需加载后仍能补齐工具、完成任务。
- 要观察真实模型在长上下文工具墙下的选择行为,请配置真实模型(见 README 中 gpt-5.6-luna 的真实结果表)。
"""
import hashlib
import json
import re
from types import SimpleNamespace
from typing import Dict, List, Tuple
from tools_library import TOOLS_BY_NAME
_DIM = 512
# ---------------------------------------------------------------------------
# 1) 本地嵌入后端
# ---------------------------------------------------------------------------
def _tokens(text: str) -> List[str]:
"""把中英文混合文本切成词袋 token:英文按词(并拆下划线),中文按字 unigram + bigram。"""
text = text.lower()
toks: List[str] = []
for w in re.findall(r"[a-z0-9]+", text):
toks.append(w)
han = re.findall(r"[一-鿿]", text)
toks += han
toks += [han[i] + han[i + 1] for i in range(len(han) - 1)]
return toks
class LocalEmbedder:
"""哈希词袋嵌入:确定性、无需联网。相似度由中英文关键词重叠驱动。"""
name = "local-hash-%d" % _DIM
def embed(self, texts: List[str]) -> List[List[float]]:
out = []
for t in texts:
vec = [0.0] * _DIM
for tok in _tokens(t):
h = int(hashlib.md5(tok.encode()).hexdigest(), 16)
vec[h % _DIM] += 1.0
norm = sum(x * x for x in vec) ** 0.5 or 1.0
out.append([x / norm for x in vec])
return out
# ---------------------------------------------------------------------------
# 2) 脚本化 mock 模型
# ---------------------------------------------------------------------------
# 意图规则:把任务关键词映射到"应当使用的专用工具"及一句能力需求描述。
# 顺序有意义(如"预报"类天气须排在通用"天气"之前)。
INTENT_RULES: List[Tuple[str, str, str]] = [
(r"股价|股票", "get_stock_price", "查询某股票的实时价格与涨跌幅"),
(r"以太坊|比特币|加密|\beth\b|\bbtc\b", "get_crypto_price", "查询加密货币的实时价格"),
(r"日元|汇率|美元.*换|换.*(日元|美元|欧元)|兑换", "get_forex_rate", "查询两种法定货币的外汇汇率"),
(r"论文|arxiv|文献|量子计算|科研进展|研究进展", "arxiv_search", "在学术论文库检索最新论文"),
(r"下载", "download_file", "从 URL 下载文件保存到本地"),
(r"贡献", "github_list_contributors", "获取 GitHub 仓库的贡献者提交统计"),
(r"图表|可视化|画个|画图|画一", "render_chart", "根据数据渲染可视化图表"),
(r"预报|未来|周日|这周|明天|后天|下周", "get_weather_forecast", "查询某城市未来若干天的天气预报"),
(r"天气", "get_current_weather", "查询某城市的实时天气"),
(r"日历|日程|活动|记一个|记录一个", "create_calendar_event", "在日历上创建一个事件"),
(r"新闻|舆论|消息|报道|风向", "search_news", "按关键词检索相关的最新新闻"),
]
def match_intents(prompt: str) -> List[Tuple[str, str]]:
"""返回任务涉及的 (专用工具名, 能力需求描述) 列表(去重、保序)。"""
needed: List[Tuple[str, str]] = []
seen = set()
for pat, tool, phrase in INTENT_RULES:
if re.search(pat, prompt, re.IGNORECASE) and tool not in seen:
needed.append((tool, phrase))
seen.add(tool)
# 天气去重:若命中"预报"则不再单独要求"实时天气"。
if "get_weather_forecast" in seen and "get_current_weather" in seen:
needed = [(t, p) for t, p in needed if t != "get_current_weather"]
return needed
_ARG_HINTS = {
"symbol": "AAPL", "location": "北京", "query": "查询", "url": "https://example.com/f.pdf",
"path": "/tmp/paper.pdf", "owner": "pytorch", "repo": "pytorch", "base": "USD",
"quote": "JPY", "title": "户外徒步", "start": "2026-07-19T09:00", "end": "2026-07-19T12:00",
"days": 3, "data": "[]", "chart_type": "bar", "code": "print('ok')", "max_results": 3,
}
def _fill_args(tool_name: str) -> Dict:
tool = TOOLS_BY_NAME.get(tool_name)
if not tool:
return {}
props = tool["function"]["parameters"]["properties"]
args = {}
for key, spec in props.items():
if key in _ARG_HINTS:
args[key] = _ARG_HINTS[key]
elif spec.get("type") == "integer":
args[key] = 1
else:
args[key] = "auto"
return args
def _extract_json(text: str):
text = text.strip()
start = text.find("{")
if start == -1:
return None
depth = 0
for i in range(start, len(text)):
if text[i] == "{":
depth += 1
elif text[i] == "}":
depth -= 1
if depth == 0:
try:
return json.loads(text[start:i + 1])
except json.JSONDecodeError:
return None
return None
def _json(thought: str, tool: str, arguments: Dict) -> str:
return json.dumps({"thought": thought, "tool": tool, "arguments": arguments},
ensure_ascii=False)
class MockChatClient:
"""确定性脚本模型;接口与 OpenAI 客户端子集兼容。"""
def __init__(self):
self.chat = SimpleNamespace(completions=SimpleNamespace(create=self._create))
def _create(self, model=None, messages=None, temperature=0, **kw):
content = self._respond(messages or [])
msg = SimpleNamespace(content=content)
return SimpleNamespace(choices=[SimpleNamespace(message=msg)])
def _respond(self, messages: List[Dict]) -> str:
system = messages[0]["content"] if messages and messages[0]["role"] == "system" else ""
task_prompt = next((m["content"] for m in messages if m["role"] == "user"), "")
full_text = "\n".join(m.get("content", "") for m in messages)
has_discover = "discover_tools" in system
# 当前"可用工具" = 出现在对话文本中的工具名(system 注入 / discover 追加)。
available = set(re.findall(r'"name":\s*"([a-zA-Z_][a-zA-Z0-9_]*)"', full_text))
available.discard("discover_tools")
prior = []
for m in messages:
if m["role"] == "assistant":
a = _extract_json(m.get("content", ""))
if a and "tool" in a:
prior.append(a)
called_ok = {a["tool"] for a in prior if a["tool"] in available}
discover_needs = [((a.get("arguments") or {}).get("need", ""))
for a in prior if a.get("tool") == "discover_tools"]
attempted = [a["tool"] for a in prior
if a["tool"] not in available and a["tool"] not in ("discover_tools", "finish")]
for tool, phrase in match_intents(task_prompt):
if tool in called_ok:
continue
if tool in available:
return _json(f"调用专用工具 {tool}", tool, _fill_args(tool))
# 目标工具当前不可用
if has_discover:
if discover_needs.count(phrase) >= 1:
continue # 已发现过仍未命中 -> 放弃该子任务
return _json(f"我需要一个能『{phrase}』的工具,先发现它", "discover_tools",
{"need": phrase})
else:
if attempted.count(tool) >= 1:
continue # 清单里没有该工具,尝试过一次即放弃
return _json(f"任务需要 {tool},尝试调用", tool, _fill_args(tool))
return _json("所有子任务已处理", "finish", {"answer": "已完成可完成的子任务。"})