|
|
@@ -0,0 +1,405 @@
|
|
|
+{
|
|
|
+ "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
|
|
|
+}
|