{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 配载智能体 StowageAgent\n", "\n", "> 一个会查规则、会算贝位的集装箱船配载助手。\n", "\n", "这个 Notebook 是**教学版**:每一步都拆开单独演示,你按顺序跑下来,\n", "就把智能体的核心模块串起来了。\n", "\n", "| 这一格在演示什么 | 涉及的能力 |\n", "|---|---|\n", "| 1. 加载配置、连上大模型 | 大模型接入 |\n", "| 2. 给智能体装上\"手\"(工具) | 工具系统 |\n", "| 3. 让智能体能\"翻资料\"(RAG) | 知识库检索 |\n", "| 4. 让智能体\"记得住\"(记忆) | 记忆机制 |\n", "| 5. 把上面几样拼成 ReAct 循环 | ReAct 循环 |\n", "| 6. 给它打分(评估) | 性能评估 |\n", "\n", "**运行前先做一件事**:把 `.env.example` 复制成 `.env`,填上你自己的模型信息。" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 0. 准备工作\n", "\n", "先把项目根目录加进 Python 的搜索路径,这样 `import src` 才找得到。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import sys\n", "from pathlib import Path\n", "\n", "# 往上找,直到找到含 src/ 的那一层,就是项目根目录\n", "ROOT = Path.cwd().resolve()\n", "while not (ROOT / \"src\").exists() and ROOT.parent != ROOT:\n", " ROOT = ROOT.parent\n", "sys.path.insert(0, str(ROOT))\n", "print(\"项目根目录:\", ROOT)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## 1. 连上大模型\n", "\n", "大模型这一层落到代码上就一句:\n", "**把 messages 发给它,拿回一段文本。**\n", "\n", "messages 里三种角色要分清:\n", "\n", "- `system` —— 定人设、定规矩(比如\"你是一名配载助理,不准瞎编\")\n", "- `user` —— 用户说的话\n", "- `assistant` —— 模型之前说过的话" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from src import LLM\n", "\n", "llm = LLM()\n", "print(\"模型:\", llm.model)\n", "\n", "reply = llm.chat([\n", " {\"role\": \"system\", \"content\": \"你是一名集装箱船配载助理,回答不超过 30 字。\"},\n", " {\"role\": \"user\", \"content\": \"一句话说明 40 尺箱为什么要写大贝号。\"},\n", "])\n", "print(reply)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## 2. 给智能体装上\"手\":工具系统\n", "\n", "大模型只会写字,**不会算数、不会查表**。\n", "你让它是\"09+11 等于几\",它可能一本正经地答错。\n", "\n", "工具就是补这个短板的。一个工具需要三样东西:\n", "\n", "| 要素 | 作用 |\n", "|---|---|\n", "| `name` | 模型用它\"点名\" |\n", "| `description` | **模型靠这段文字决定什么时候用它**(它看不见代码) |\n", "| `run()` | 真正干活的逻辑 |\n", "\n", "下面 4 个工具都是纯逻辑,**不调用大模型**,所以能当单元测试来测。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from src import BayTool, CoordTool, StowageCheckTool\n", "\n", "bay = BayTool()\n", "print(\"【贝位计算器】\")\n", "print(bay.run(\"09+11\"))\n", "print()\n", "print(bay.run(\"06\"))\n", "print()\n", "print(bay.run(\"01+04\")) # 故意给个错的,看它会不会报错" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "coord = CoordTool()\n", "print(\"【坐标解析器】六位码 → 三要素\")\n", "print(coord.run(\"010682\"))\n", "print()\n", "print(\"【坐标解析器】三要素 → 六位码(注意前导零)\")\n", "print(coord.run(\"1,6,82\"))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "checker = StowageCheckTool()\n", "print(\"【箱位校验器】故意混进几个错,看它能不能查出来\")\n", "print(checker.run(\"\"\"\n", "HE 010682\n", "f 010304\n", "NE 040406\n", "NE 040406\n", "\"\"\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## 3. 让智能体能\"翻资料\":RAG\n", "\n", "**RAG = 开卷考试。** 不给资料,模型只能凭记忆硬答(很容易编);\n", "先把资料翻出来贴到问题前面,再让它答,就靠谱多了。\n", "\n", "RAG 四步,`src/rag.py` 里每一步都是一个独立方法:\n", "\n", "1. **分块** —— 把一大篇资料切成小段(本项目:一个空行 = 一段)\n", "2. **建索引** —— 给每段算一个\"指纹\"\n", "3. **检索** —— 把问题也算成指纹,比一比谁最像\n", "4. **拼上下文** —— 把命中的几段贴到问题前面(`src/context.py` 负责)\n", "\n", "后面两行都在问同一个问题,**但一次都没提到\"前导零\"这三个字**,\n", "你看看它能不能靠语义找对——这就是\"第一代 vs 第二代\"。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from src import Retriever\n", "\n", "kb = Retriever(str(ROOT / \"data\" / \"配载知识库.txt\"), mode=\"tfidf\")\n", "kb.index()\n", "print(f\"知识库共 {len(kb.chunks)} 段资料\\n\")\n", "\n", "for text, score in kb.search(\"坐标写成数字会丢东西吗\", top_k=2):\n", " print(f\"[{score:.3f}] {text[:80]}…\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 换成真·语义向量检索:更能理解\"意思\",但首次要下载约 400MB 模型\n", "# 建议先把上面 tfidf 的结果记下来,再跑这个对比\n", "# kb2 = Retriever(str(ROOT / \"data\" / \"配载知识库.txt\"), mode=\"vector\")\n", "# kb2.index()\n", "# for text, score in kb2.search(\"坐标写成数字会丢东西吗\", top_k=2):\n", "# print(f\"[{score:.3f}] {text[:80]}…\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## 4. 让智能体\"记得住\":记忆\n", "\n", "记忆可以分好几类,落到工程上先抓住两层:\n", "\n", "- **短期记忆** —— 这次对话说过什么。就是 messages 列表,有长度上限,超了丢最早的\n", "- **长期记忆** —— 跨对话要记住的事。写进一个文件,下次打开还在\n", "\n", "区别一句话:短期记忆是\"刚才聊到哪了\",长期记忆是\"这个人有什么习惯\"。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from src import Memory\n", "\n", "mem = Memory(note_path=ROOT / \"outputs\" / \"长期笔记.md\", max_messages=4)\n", "for i in range(1, 7):\n", " mem.add(\"user\", f\"第{i}句话\")\n", "print(\"故意说了 6 句,max_messages=4,所以只剩最近 4 句:\")\n", "print(mem.recent())\n", "\n", "mem.add_note(\"「安洋66」轮的占位符号是星号 *,不是 X\")\n", "print(\"\\n长期笔记:\", mem.recall())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## 5. 全部拼起来:ReAct 循环\n", "\n", "**ReAct = Reason(想)+ Act(做)。**\n", "\n", "它和普通聊天的区别就一句话:普通聊天是\"问一句答一句\",\n", "ReAct 是\"想一步、做一步、看结果、再想下一步\",能循环好几轮。\n", "\n", "```\n", "问题 → 思考 → 行动(调工具)→ 观察(工具结果)→ 还想继续吗?\n", " ↓ 不用了\n", " 最终答案\n", "```\n", "\n", "下面这格会打印出它的**每一步思考**(叫 trace),你重点看两件事:\n", "\n", "1. 它会不会**选对工具**?(该算贝位的时候有没有去查知识库)\n", "2. 它会不会**瞎编**?(资料里没有的东西,它是不是老实说\"没有提到\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from src import (BayTool, CoordTool, KnowledgeTool, Memory, ReActAgent,\n", " Retriever, StowageCheckTool, ToolRegistry)\n", "\n", "# ① 建检索器\n", "kb = Retriever(str(ROOT / \"data\" / \"配载知识库.txt\"), mode=\"tfidf\")\n", "kb.index()\n", "\n", "# ② 注册工具\n", "registry = ToolRegistry()\n", "registry.register(BayTool())\n", "registry.register(CoordTool())\n", "registry.register(StowageCheckTool())\n", "registry.register(KnowledgeTool(kb, top_k=3))\n", "\n", "# ③ 组装 Agent\n", "agent = ReActAgent(\n", " llm=llm,\n", " registry=registry,\n", " memory=Memory(note_path=ROOT / \"outputs\" / \"长期笔记.md\"),\n", " max_steps=5,\n", " verbose=True, # 打印每一轮的思考过程\n", ")\n", "\n", "# ④ 问它一个\"既要用工具、又要查资料\"的复合问题\n", "result = agent.run(\"贝位 09 和 11 合成的大贝是几号?层号 84 的箱子在甲板上还是船舱里?\")\n", "print(\"\\n👉 最终答案:\", result[\"answer\"])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 再看看它面对\"资料里没有的问题\"会怎么答\n", "\n", "这一格最能测出一个 Agent 靠不靠谱。\n", "普通聊天机器人会一本正经地编一个答案,RAG 应该老实说\"没有提到\"。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "result = agent.run(\"宁波到洛杉矶一个 40 尺柜多少钱?\")\n", "print(\"\\n👉 最终答案:\", result[\"answer\"])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## 6. 给它打分:性能评估\n", "\n", "评估方法论有很多(BFCL 测工具调用、GAIA 测通用能力……)。\n", "小项目抓住一个核心就够:\n", "\n", "> **把\"感觉它挺好\"变成\"跑 20 个用例,过了 17 个\"。**\n", "\n", "这里做了两层,是很典型的\"分层测试\"思路:\n", "\n", "| 层 | 要联网吗 | 结果确定吗 | 作用 |\n", "|---|---|---|---|\n", "| 工具自检 | 不要 | **确定**(算错就是算错) | 改代码后最快的安全网 |\n", "| 端到端评估 | 要 | 会抖动 | 测真实的问答能力 |\n", "\n", "先跑免费的那层。" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from src import check_tools\n", "from src.evaluate import print_tool_report\n", "\n", "report = check_tools(registry, ROOT / \"data\" / \"测试用例.json\")\n", "print_tool_report(report)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 端到端评估:每一题都要调一次大模型,慢而且花钱,想清楚再跑\n", "# from src import evaluate_agent\n", "# from src.evaluate import print_agent_report\n", "# r = evaluate_agent(agent.run, ROOT / \"data\" / \"测试用例.json\")\n", "# print_agent_report(r)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## 7. 小结\n", "\n", "跑完这个 Notebook,你已经动手做过一遍:\n", "\n", "1. 怎么连大模型\n", "2. 怎么把业务能力包装成工具\n", "3. 怎么用 RAG 让模型\"翻资料\"\n", "4. 怎么给 Agent 加记忆\n", "5. 怎么把它们拼成 ReAct 循环(第 1、4 章)\n", "6. 怎么评估它到底行不行\n", "\n", "**下一步可以自己改的地方:**\n", "\n", "- 往 `data/配载知识库.txt` 里加你自己的规则 —— 一个空行加一段,不用改代码\n", "- 往 `data/测试用例.json` 里加测试用例 —— 加完就知道新规则有没有被\"学会\"\n", "- 把 `--mode` 换成 `vector` —— 体验\"第二代检索\"强在哪\n", "\n", "> 📌 提醒:这是个**学习脚手架**,不是干活的生产工具。\n", "> 真实场景知识库有几万段时,就该换向量数据库(Qdrant)了。" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.11" } }, "nbformat": 4, "nbformat_minor": 5 }