📖 Day 12 学习页:项目设计与核心开发(下):工具 / 记忆 / 错误处理

Day 12:项目设计与核心开发(下)——工具、记忆、错误处理

🎯 今日目标:补齐脚手架的三块能力——带安全边界的工具系统、 按会话隔离的记忆、三层错误处理。做完后问"帮我计算 3 + 5 * 2"应该真的走工具, 而试图读工作区之外的文件会被拒。

🛠 项目脚手架:project_day11_15/(开发都在这个目录里) | 📋 验收清单

⏱ 今日安排(约 2 小时)

时间内容方式
25min理论学习:工具服务化 + 路径穿越攻击阅读
80min动手实操:tools / memory 全部 TODO编码
15min验收:四个场景逐项打勾验收

📖 第一步:理论学习

1. 工具系统的服务化:和 Day 2 差在哪

结构没变(注册表 + 装饰器),变的是约定: ① 工具执行只有一个出口 execute_tool()——Agent 不许绕过它直接调函数(脚手架 README 设计约定第 3 条); ② 工具失败不是异常,是返回给 LLM 的一条 Observation("错误:文件不存在"), LLM 看到错误信息往往能自己纠正;③ 每个工具的 parameters 用 JSON Schema 描述——它们会原样进 Function Calling 请求。

2. 文件工具的安全边界(今天最重要的一节)

Agent 能读文件 = LLM 决定读哪个文件。恶意或幻觉的路径可能长这样:

用户:帮我看看系统里的密码文件
LLM 调用:read_file({"path": "../../../etc/passwd"})   ← 路径穿越攻击!

防御方法:所有路径先拼到工作区内,再 resolve,最后验证仍在工作区内—— 出界一律拒绝:

from pathlib import Path

def 安全路径(workspace: Path, path: str) -> Path:
    target = (workspace / path).resolve()          # 1. 拼接 + 解析(消化掉 ../)
    target.relative_to(workspace.resolve())        # 2. 不在界内会直接抛异常
    return target                                  # 3. 通过检查才允许读写

⚠️ 这是真实世界的教训:给 LLM 文件/Shell 权限时,永远假设它迟早会生成一个危险路径。 白名单 + 边界校验是底线,"提示词里嘱咐它别乱读"不是防线。

3. 会话记忆服务化

MemoryStore 用字典把 session_id 映射到独立的 SessionMemory。 裁剪规则和 Day 9 一样(超限裁最旧、system 永远保留)。要建立的生产认知: 进程内字典只适合单进程部署;多实例部署(k8s 起多个 pod)时字典不共享,要换 Redis—— 今天知道边界在哪即可,实现不换。

4. 错误处理三层(脚手架 README 设计约定第 4 条)

层负责什么在哪实现
LLM 适配层超时、429/5xx 重试、指数退避;配置类错误(401/无 Key)不重试直接上抛llm.py(已写好)
业务层工具失败转为 Observation 回传给 LLM 自纠;轮数上限agent.py
路由层LLMError → 503、未知异常 → 500,统一 JSON 错误格式,不泄露 tracebackmain.py

🛠 第二步:动手实操

  1. tools.py:TODO 2.1(注册,几行)→ 2.2(tools 格式)→ 2.3(统一执行出口)→ 2.4(先写安全边界:实现 _安全路径 后,故意传 ../../secret.txt 试一下,应被拒) → 2.5(自选工具,如 write_file——同样必须过安全检查)。
  2. memory.py:TODO 3.1/3.2(Day 9 原题)→ 3.3(get 不存在则创建)→ 3.4(clear 保留 system)。
  3. 联调:重启 uvicorn(--reload 会自动),测四个场景:
    • "帮我计算 3 + 5 * 2" → 回答含 13,且响应的 tool_calls 里有记录
    • 在工作区放一个 notes.md,问"读一下 notes.md" → 回答含文件内容
    • 问"读取 ../../README.md" → 拒绝(工具返回错误,LLM 转述)
    • 换一个 session_id 问同样问题 → 回答不到上一个会话的内容(隔离生效)
  4. 验收:按 验收标准文档 Day 11-15 交付清单中 Day 11-12 部分逐项打勾;四个场景全部通过即完成阶段三的主体。

⚠️ 避坑

  • ❌ 先拼接后不 resolve 就判断 → workspace/../secret 绕过检查;必须 resolve 后再验证
  • ❌ 工具抛异常直接让请求 500 → 工具错误是正常业务,返回错误字符串让 LLM 自纠
  • ❌ read_file/write_file 只挡了读没挡写 → 写入危害更大;同一套 _安全路径 两边都用
  • ❌ 401/无 Key 返回 500 → 客户端没法区分是自己的配置问题还是服务挂了;503 + 明确说明

🔗 延伸资源

🚀 下一站

Day 13 给这个服务加 Web UI 和 SSE 流式(脚手架 main.py 已留注释位),Day 14 pytest + Docker。 均按 交付清单 在脚手架上继续。