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
+220
View File
@@ -0,0 +1,220 @@
"""离线演示:不依赖任何 LLM / API key,直接驱动异步运行时的底层原语。
`demo.py` 里的四个「场景」需要真实 LLM 做决策;本模块则把实验 6-2 的三项核心
异步能力单独拎出来,用可测量、可复现的方式演示,**无需联网、无需 API key**
- demo_parallel :并行 vs 串行工具调用的【墙钟时间】对比(真实测量,打印加速比)。
- demo_interrupt :长任务运行中被【打断/取消】,随后系统【恢复】并接受新任务。
- demo_state Agent 状态【检查点持久化】到磁盘,再【跨会话恢复】并校验。
这三个演示共同回答「异步到底带来了什么」——用数字和状态变化说话,而不只是措辞。
"""
from __future__ import annotations
import asyncio
import datetime
import os
import time
from runtime import AgentRuntime, format_log
from events import Event, EventType
from tasks import TaskManager
import tasks
class Logger:
"""与 runtime 同款的彩色时间戳日志器(相对本次演示起点计时)。"""
def __init__(self) -> None:
self.t0 = time.time()
def __call__(self, source: str, text: str) -> None:
print(format_log(self.t0, source, text), flush=True)
def banner(title: str) -> None:
print("\n" + "=" * 78)
print(f" {title}")
print("=" * 78, flush=True)
# ============================ 1. 并行 vs 串行 ============================
# 一组相互独立的【只读感知工具】(读文件 / 搜索 / 查库 / 向量检索)。
# 只读、无副作用,因此可以安全地并行——这正是书中「感知工具天然适合并行」的落点。
_PERCEIVE_TOOLS = [
("read_config.json", 0.8),
("web_search(‘异步 Agent)", 1.2),
("db_query(orders)", 1.5),
("vector_lookup(memory)", 1.0),
]
async def _perceive(name: str, latency: float, log: Logger) -> tuple[str, float, float]:
"""模拟一次带 I/O 延迟的只读感知调用;返回 (名称, 标称延迟, 实测耗时)。"""
t0 = time.time()
log("TOOL", f"{name} 启动(模拟 I/O 耗时 {latency:.1f}s")
await asyncio.sleep(latency)
dt = time.time() - t0
log("TOOL", f"{name} 完成(实测 {dt:.2f}s")
return name, latency, dt
async def demo_parallel() -> None:
banner("能力一|并行工具调用:并行 vs 串行的墙钟时间对比")
log = Logger()
log("SYSTEM", "有 4 个相互独立的只读感知工具需要调用(无副作用,可安全并行)。")
# —— 串行:一个 await 完再 await 下一个 ——
log("SYSTEM", "\033[0m[串行] 逐个 await(同步 ReAct 的默认做法)……")
seq_start = time.time()
for name, lat in _PERCEIVE_TOOLS:
await _perceive(name, lat, log)
seq_total = time.time() - seq_start
# —— 并行:一次性发起,asyncio.gather 并发等待 ——
log("SYSTEM", "\033[0m[并行] 一次性发起,asyncio.gather 并发等待……")
par_start = time.time()
await asyncio.gather(*[_perceive(name, lat, log) for name, lat in _PERCEIVE_TOOLS])
par_total = time.time() - par_start
slowest = max(lat for _, lat in _PERCEIVE_TOOLS)
speedup = seq_total / par_total if par_total else float("inf")
print("\n ── 结果对比 ─────────────────────────────────────────────")
print(f" {'工具':<26}{'标称延迟':>10}")
for name, lat in _PERCEIVE_TOOLS:
print(f" {name:<26}{lat:>8.1f}s")
print(" ─────────────────────────────────────────────────────────")
print(f" {'串行总耗时(Σ 各工具)':<26}{seq_total:>8.2f}s")
print(f" {'并行总耗时(gather':<26}{par_total:>8.2f}s")
print(f" {'并行理论下界(最慢单个)':<26}{slowest:>8.2f}s")
print(f" {'加速比 = 串行 / 并行':<26}{speedup:>8.2f}x")
print(" ─────────────────────────────────────────────────────────")
print(" 结论:独立的只读调用并行化后,墙钟时间由「求和」降到「取最大」。\n")
# ============================ 2. 打断 / 取消 / 恢复 ============================
async def demo_interrupt() -> None:
banner("能力二|打断与取消:长任务运行中被打断,随后系统恢复")
tasks.TICK_REAL = 0.15 # 本演示放慢节奏,留出「跑到一半再打断」的时间窗口
log = Logger()
completed: list = []
async def on_complete(state) -> None:
completed.append(state)
tm = TaskManager(on_complete=on_complete, log=log)
# 1) 并行启动三个后台异步任务
log("SYSTEM", "启动三个并行后台分析任务(fast/mid/slow)……")
for cmd in ["python analyze_fast.py", "python analyze_mid.py", "python analyze_slow.py"]:
tm.start(cmd)
# 2) 运行期间用户即时提问 —— 后台任务不被阻塞
await asyncio.sleep(1.0)
now = datetime.datetime.now().strftime("%H:%M:%S")
log("USER", "(即时提问)现在几点了?")
log("AGENT", f"现在 {now}。三个后台任务仍在并行推进,未被这次提问阻塞。")
# 3) 跑到中途,用户发出打断 —— 立即取消所有在跑的任务
await asyncio.sleep(1.0)
log("USER", "(打断)取消")
cancelled = tm.cancel_all()
await asyncio.sleep(0.05) # 让 CancelledError 在各协程内落地
log("SYSTEM", f"已执行打断:取消了 {cancelled}(进度在被取消处冻结)")
print("\n ── 打断后各任务状态(进度冻结在中途)───────────────────")
print(f" {'task_id':<8}{'命令':<26}{'状态':<12}{'进度':>6}")
for s in tm.all_states():
print(f" {s.task_id:<8}{s.command:<26}{s.status:<12}{s.progress:>5.0f}%")
print(" ─────────────────────────────────────────────────────────")
# 4) 恢复:executor 依然健康,接受并跑完一个新任务
log("SYSTEM", "打断处理完毕,系统恢复空闲,可继续接受新任务……")
fresh = tm.start("python re_run_summary.py")
await fresh._task
log("AGENT", f"已从打断中恢复,新任务 {fresh.task_id} 正常完成:"
f"{completed[-1].result[:36]}……")
print(" 结论:打断只冻结被取消的任务,运行时本身无损,可立即继续工作。\n")
# ============================ 3. 状态检查点:持久化 / 恢复 ============================
def _seed_trajectory(rt: AgentRuntime) -> None:
"""给运行时灌入一段「已发生」的对话轨迹,模拟会话进行到一半。"""
rt._append(Event(EventType.USER_INPUT,
message={"role": "user", "content": "分析今天的日志并总结异常"},
label="用户消息:分析日志"))
rt._append(Event(EventType.AGENT_TOOL_CALL,
message={"role": "assistant", "content": "好的,我这就在后台启动分析。",
"tool_calls": [{"id": "call_1", "type": "function",
"function": {"name": "run_terminal_command",
"arguments": '{"command": "python analyze_fast.py"}'}}]},
label="调用工具 run_terminal_command"))
rt._append(Event(EventType.TOOL_RESULT,
message={"role": "tool", "tool_call_id": "call_1",
"content": "命令已在后台异步启动。task_id=T1。"},
label="工具结果 run_terminal_command"))
async def demo_state() -> None:
banner("能力三|状态管理:检查点持久化与跨会话恢复")
tasks.TICK_REAL = 0.15
ckpt_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "checkpoints")
os.makedirs(ckpt_dir, exist_ok=True)
path = os.path.join(ckpt_dir, "agent_state.json")
# —— 会话 A:产生一段轨迹 + 两个仍在运行的后台任务,然后落盘 ——
log = Logger()
log("SYSTEM", "会话 A 开始:构造轨迹并启动两个后台任务……")
rt_a = AgentRuntime(client=None, model="demo-offline")
rt_a._t0 = log.t0 # 让两个运行时共用同一时间基准,便于观察
_seed_trajectory(rt_a)
rt_a.tasks.start("python analyze_fast.py") # 进行中
rt_a.tasks.start("python analyze_slow.py") # 进行中
await asyncio.sleep(1.2) # 让进度累积到中途
before_traj = len(rt_a.trajectory)
before_tasks = {s.task_id: (s.status, s.progress) for s in rt_a.tasks.all_states()}
rt_a.save_checkpoint(path)
# 模拟进程退出:取消掉活着的协程
rt_a.tasks.cancel_all()
await asyncio.sleep(0.05)
log("SYSTEM", "会话 A 结束(进程退出,内存中的运行时已销毁)。")
# —— 会话 B:全新运行时,从磁盘恢复 ——
log("SYSTEM", "会话 B 开始:新建空运行时,从检查点恢复……")
rt_b = AgentRuntime(client=None, model="demo-offline")
rt_b._t0 = log.t0
data = rt_b.load_checkpoint(path)
after_traj = len(rt_b.trajectory)
msgs = rt_b.build_messages() # 证明恢复后能重建可喂给 LLM 的上下文
print("\n ── 恢复校验 ─────────────────────────────────────────────")
print(f" 轨迹事件数 保存前 {before_traj} -> 恢复后 {after_traj} "
f"[{'一致 ✓' if before_traj == after_traj else '不一致 ✗'}]")
print(f" 可重建 LLM 上下文消息 {len(msgs)} 条(system + 轨迹回放)")
print(f" {'task_id':<8}{'命令':<26}{'保存前进度':>10} {'恢复后状态':<12}{'进度':>6}")
for rec in data["tasks"]:
tid = rec["task_id"]
before = before_tasks.get(tid, ("-", 0.0))
st = rt_b.tasks.query(tid)
print(f" {tid:<8}{rec['command']:<26}{before[1]:>9.0f}% "
f"{st.status:<12}{st.progress:>5.0f}%")
print(" ─────────────────────────────────────────────────────────")
print(f" 检查点文件:{path}")
print(" 结论:轨迹与任务进度完整落盘并跨会话还原;运行中的任务被标记为 suspended,")
print(" 保留了最后已知进度,供上层决定「重跑」还是「按进度续跑」。\n")
# 供 demo.py 复用的离线演示注册表
OFFLINE_DEMOS = {
"parallel": demo_parallel,
"interrupt": demo_interrupt,
"state": demo_state,
}