📖 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 错误格式,不泄露 traceback | main.py |
🛠 第二步:动手实操
- tools.py:TODO 2.1(注册,几行)→ 2.2(tools 格式)→ 2.3(统一执行出口)→
2.4(先写安全边界:实现
_安全路径后,故意传../../secret.txt试一下,应被拒) → 2.5(自选工具,如 write_file——同样必须过安全检查)。 - memory.py:TODO 3.1/3.2(Day 9 原题)→ 3.3(get 不存在则创建)→ 3.4(clear 保留 system)。
- 联调:重启 uvicorn(--reload 会自动),测四个场景:
- "帮我计算 3 + 5 * 2" → 回答含 13,且响应的 tool_calls 里有记录
- 在工作区放一个
notes.md,问"读一下 notes.md" → 回答含文件内容 - 问"读取 ../../README.md" → 拒绝(工具返回错误,LLM 转述)
- 换一个 session_id 问同样问题 → 回答不到上一个会话的内容(隔离生效)
- 验收:按 验收标准文档 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。 均按 交付清单 在脚手架上继续。