Просмотр исходного кода

Merge branch 'datawhalechina:main' into main

J.Jason 3 месяцев назад
Родитель
Сommit
46dbbff053
100 измененных файлов с 6096 добавлено и 69 удалено
  1. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/.env.example
  2. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/.gitignore
  3. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/README.md
  4. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/data/sample_queries.txt
  5. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/main.ipynb
  6. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/main.py
  7. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/outputs/demo_results.md
  8. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/outputs/docs/CONTRIBUTING.md
  9. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/outputs/docs/IMPROVEMENTS_SUMMARY.md
  10. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/outputs/tests/test_agent_improvements.py
  11. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/outputs/tests/test_tools.py
  12. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/requirements.txt
  13. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/src/__init__.py
  14. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/src/agents/__init__.py
  15. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/src/agents/agent_universal.py
  16. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/src/agents/config.py
  17. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/src/tools/__init__.py
  18. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/src/tools/browser_tool.py
  19. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/src/tools/terminal_tool.py
  20. 0 0
      Co-creation-projects/haoye2-UnivesalAgent/src/utils/__init__.py
  21. 9 0
      Co-creation-projects/kkkano-FinReportAgent/.env.example
  22. 105 0
      Co-creation-projects/kkkano-FinReportAgent/README.md
  23. 559 0
      Co-creation-projects/kkkano-FinReportAgent/main.ipynb
  24. 25 0
      Co-creation-projects/kkkano-FinReportAgent/requirements.txt
  25. 18 0
      Co-creation-projects/lgs-only-NovelGenerator/.env.example
  26. 142 0
      Co-creation-projects/lgs-only-NovelGenerator/README.md
  27. 381 0
      Co-creation-projects/lgs-only-NovelGenerator/agents/chapter_generate_agent.py
  28. 161 0
      Co-creation-projects/lgs-only-NovelGenerator/agents/outline_agent.py
  29. 174 0
      Co-creation-projects/lgs-only-NovelGenerator/agents/prompt.py
  30. BIN
      Co-creation-projects/lgs-only-NovelGenerator/data/image.png
  31. 605 0
      Co-creation-projects/lgs-only-NovelGenerator/frontend/index.html
  32. 139 0
      Co-creation-projects/lgs-only-NovelGenerator/main.py
  33. 40 0
      Co-creation-projects/lgs-only-NovelGenerator/outputs/测试Agent功能小说-test_novel_1769540842/chapters/note_20260128_030815_0.md
  34. 17 0
      Co-creation-projects/lgs-only-NovelGenerator/outputs/测试Agent功能小说-test_novel_1769540842/chapters/notes_index.json
  35. 81 0
      Co-creation-projects/lgs-only-NovelGenerator/outputs/测试Agent功能小说-test_novel_1769540842/outline/note_20260128_030758_0.md
  36. 17 0
      Co-creation-projects/lgs-only-NovelGenerator/outputs/测试Agent功能小说-test_novel_1769540842/outline/notes_index.json
  37. 11 0
      Co-creation-projects/lgs-only-NovelGenerator/requirements.txt
  38. 261 0
      Co-creation-projects/lgs-only-NovelGenerator/src/app.py
  39. 33 0
      Co-creation-projects/pamdla-MindEchoAgent/Dockerfile
  40. 164 0
      Co-creation-projects/pamdla-MindEchoAgent/README.md
  41. 41 0
      Co-creation-projects/pamdla-MindEchoAgent/docker-compose.yaml
  42. 6 0
      Co-creation-projects/pamdla-MindEchoAgent/main.ipynb
  43. 222 0
      Co-creation-projects/pamdla-MindEchoAgent/main.py
  44. 8 0
      Co-creation-projects/pamdla-MindEchoAgent/requirements.txt
  45. 46 0
      Co-creation-projects/pamdla-MindEchoAgent/src/agents/mind_echo_agent.py
  46. 20 0
      Co-creation-projects/pamdla-MindEchoAgent/src/agents/sleep_agent.py
  47. 33 0
      Co-creation-projects/pamdla-MindEchoAgent/src/tools/dialogue_state_tool.py
  48. 48 0
      Co-creation-projects/pamdla-MindEchoAgent/src/tools/mood_music_tool.py
  49. 31 0
      Co-creation-projects/pamdla-MindEchoAgent/src/tools/mood_summary_tool.py
  50. 31 0
      Co-creation-projects/pamdla-MindEchoAgent/src/tools/text_comfort_tool.py
  51. 9 0
      Co-creation-projects/pamdla-MindEchoAgent/src/utils/loader.py
  52. 20 0
      Co-creation-projects/pamdla-MindEchoAgent/src/utils/state.py
  53. 364 0
      Extra-Chapter/Extra07-环境配置.md
  54. 745 0
      Extra-Chapter/Extra08-如何写出好的Skill.md
  55. 1341 0
      Extra-Chapter/Extra09-Agent应用开发实践踩坑与经验分享.md
  56. BIN
      Extra-Chapter/images/Extra07-figures/image1.png
  57. BIN
      Extra-Chapter/images/Extra07-figures/image2.png
  58. BIN
      Extra-Chapter/images/Extra07-figures/image3.png
  59. BIN
      Extra-Chapter/images/Extra07-figures/image4.png
  60. BIN
      Extra-Chapter/images/Extra07-figures/image5.png
  61. BIN
      Extra-Chapter/images/Extra07-figures/image6.png
  62. BIN
      Extra-Chapter/images/Extra07-figures/image7.png
  63. BIN
      Extra-Chapter/images/Extra07-figures/image8.png
  64. BIN
      Extra-Chapter/images/Extra08-figures/creation-flow.png
  65. BIN
      Extra-Chapter/images/Extra08-figures/file-interaction.png
  66. BIN
      Extra-Chapter/images/Extra08-figures/freedom-spectrum.png
  67. BIN
      Extra-Chapter/images/Extra08-figures/skill-structure.png
  68. BIN
      Extra-Chapter/images/Extra08-figures/toc.png
  69. BIN
      Extra-Chapter/images/Extra09-figures/1.png
  70. BIN
      Extra-Chapter/images/Extra09-figures/2.png
  71. BIN
      Extra-Chapter/images/Extra09-figures/3.png
  72. BIN
      Extra-Chapter/images/Extra09-figures/4.png
  73. BIN
      Extra-Chapter/images/Extra09-figures/5.png
  74. BIN
      Extra-Chapter/images/Extra09-figures/6.png
  75. 20 13
      README.md
  76. 16 4
      README_EN.md
  77. 1 1
      code/chapter13/helloagents-trip-planner/backend/requirements.txt
  78. 1 1
      code/chapter14/helloagents-deepresearch/backend/pyproject.toml
  79. 1 1
      code/chapter15/Helloagents-AI-Town/backend/requirements.txt
  80. 1 0
      code/chapter16/共创路径.md
  81. 6 4
      code/chapter4/ReAct.py
  82. 2 2
      code/chapter8/01_MemoryTool_Basic_Operations.py
  83. 2 2
      code/chapter8/02_MemoryTool_Architecture.py
  84. 3 2
      code/chapter8/06_Memory_Consolidation_Demo.py
  85. 2 2
      code/chapter8/08_Agent_Tool_Integration.py
  86. 2 2
      code/chapter8/09_Memory_Types_Deep_Dive.py
  87. 2 2
      code/chapter8/11_Q&A_Assistant.py
  88. 0 4
      docs/Preface.md
  89. 16 4
      docs/README.md
  90. 16 4
      docs/README_EN.md
  91. 1 1
      docs/_sidebar_en.md
  92. 1 1
      docs/chapter1/Chapter1-Introduction-to-Agents.md
  93. 1 1
      docs/chapter1/第一章 初识智能体.md
  94. 1 1
      docs/chapter3/第三章 大语言模型基础.md
  95. 10 6
      docs/chapter4/Chapter4-Building-Classic-Agent-Paradigms.md
  96. 11 7
      docs/chapter4/第四章 智能体经典范式构建.md
  97. 68 0
      docs/chapter7/Chapter7-Building-Your-Agent-Framework.md
  98. 5 4
      docs/chapter7/第七章 构建你的Agent框架.md
  99. BIN
      docs/images/star-history-2026113.png
  100. BIN
      docs/images/star-history-2026210.png

+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/.env.example → Co-creation-projects/haoye2-UnivesalAgent/.env.example


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/.gitignore → Co-creation-projects/haoye2-UnivesalAgent/.gitignore


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/README.md → Co-creation-projects/haoye2-UnivesalAgent/README.md


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/data/sample_queries.txt → Co-creation-projects/haoye2-UnivesalAgent/data/sample_queries.txt


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/main.ipynb → Co-creation-projects/haoye2-UnivesalAgent/main.ipynb


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/main.py → Co-creation-projects/haoye2-UnivesalAgent/main.py


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/outputs/demo_results.md → Co-creation-projects/haoye2-UnivesalAgent/outputs/demo_results.md


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/outputs/docs/CONTRIBUTING.md → Co-creation-projects/haoye2-UnivesalAgent/outputs/docs/CONTRIBUTING.md


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/outputs/docs/IMPROVEMENTS_SUMMARY.md → Co-creation-projects/haoye2-UnivesalAgent/outputs/docs/IMPROVEMENTS_SUMMARY.md


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/outputs/tests/test_agent_improvements.py → Co-creation-projects/haoye2-UnivesalAgent/outputs/tests/test_agent_improvements.py


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/outputs/tests/test_tools.py → Co-creation-projects/haoye2-UnivesalAgent/outputs/tests/test_tools.py


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/requirements.txt → Co-creation-projects/haoye2-UnivesalAgent/requirements.txt


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/src/__init__.py → Co-creation-projects/haoye2-UnivesalAgent/src/__init__.py


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/src/agents/__init__.py → Co-creation-projects/haoye2-UnivesalAgent/src/agents/__init__.py


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/src/agents/agent_universal.py → Co-creation-projects/haoye2-UnivesalAgent/src/agents/agent_universal.py


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/src/agents/config.py → Co-creation-projects/haoye2-UnivesalAgent/src/agents/config.py


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/src/tools/__init__.py → Co-creation-projects/haoye2-UnivesalAgent/src/tools/__init__.py


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/src/tools/browser_tool.py → Co-creation-projects/haoye2-UnivesalAgent/src/tools/browser_tool.py


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/src/tools/terminal_tool.py → Co-creation-projects/haoye2-UnivesalAgent/src/tools/terminal_tool.py


+ 0 - 0
code/chapter16/haoye2-UnivesalAgent/src/utils/__init__.py → Co-creation-projects/haoye2-UnivesalAgent/src/utils/__init__.py


+ 9 - 0
Co-creation-projects/kkkano-FinReportAgent/.env.example

@@ -0,0 +1,9 @@
+# FinReportAgent 环境配置模板
+# 复制此文件为 .env 并填入你的配置
+
+# LLM 配置(必需)
+LLM_MODEL_ID=deepseek-chat
+LLM_API_KEY=your-api-key-here
+LLM_BASE_URL=https://api.deepseek.com/v1
+
+# 注:DuckDuckGo 和 Yahoo Finance 无需 API Key

+ 105 - 0
Co-creation-projects/kkkano-FinReportAgent/README.md

@@ -0,0 +1,105 @@
+# FinReportAgent - 金融研报智能体
+
+> 基于 HelloAgents 框架的金融研报生成智能体,自动收集多源数据并生成投资分析报告
+
+## 项目简介
+
+FinReportAgent 是一个基于 [HelloAgents](https://github.com/datawhalechina/hello-agents) 框架构建的金融研报生成智能体。它能够:
+
+- **自动收集数据**:通过 DuckDuckGo 搜索、Yahoo Finance API 获取实时行情和新闻
+- **智能分析推理**:基于 ReAct 范式进行多步推理,生成专业的投资分析
+- **结构化报告**:自动生成带情绪判断的 Markdown 格式研报
+
+## 核心功能
+
+- 📊 **股票价格查询** - Yahoo Finance 实时行情
+- 📰 **金融新闻搜索** - DuckDuckGo 新闻抓取
+- 🔍 **多源信息检索** - DuckDuckGo 网络搜索
+- 📄 **Markdown 报告生成** - 自动生成结构化投资分析报告
+- 📈 **情绪判断** - 自动识别看涨/看跌/中性情绪
+
+## 技术栈
+
+| 组件 | 技术 |
+|------|------|
+| 智能体框架 | [HelloAgents](https://github.com/datawhalechina/hello-agents) |
+| 智能体范式 | ReAct (Reasoning and Acting) |
+| 搜索工具 | DuckDuckGo Search |
+| 金融数据 | Yahoo Finance API (yfinance) |
+| LLM | DeepSeek / OpenAI 兼容 API |
+
+## 快速开始
+
+### 环境要求
+
+- Python 3.10+
+- Jupyter Notebook / JupyterLab
+
+### 安装依赖
+
+```bash
+pip install -r requirements.txt
+```
+
+### 配置 API 密钥
+
+**方式一:使用 .env 文件(推荐)**
+
+```bash
+# 复制配置模板
+cp .env.example .env
+
+# 编辑 .env 文件,填入你的 API 密钥
+```
+
+**方式二:直接在 Notebook 中配置**
+
+打开 `main.ipynb`,在第一个代码 Cell 中修改:
+```python
+os.environ["LLM_API_KEY"] = "your-api-key-here"  # 替换为你的 API Key
+```
+
+### 运行项目
+
+```bash
+# 启动 Jupyter
+jupyter lab
+
+# 打开 main.ipynb 并按顺序运行所有 Cell
+```
+
+## 项目结构
+
+```
+kkkano-FinReportAgent/
+├── main.ipynb         # 主程序
+├── README.md          # 项目说明
+├── requirements.txt   # 依赖列表
+└── .env.example       # 环境变量示例
+```
+
+## HelloAgents 框架组件
+
+本项目使用了 HelloAgents 框架的以下核心组件:
+
+| 组件 | 用途 |
+|------|------|
+| `ReActAgent` | ReAct 循环框架(推理-行动-观察) |
+| `HelloAgentsLLM` | 统一的 LLM 调用接口 |
+| `ToolRegistry` | 工具注册和管理 |
+| `Tool` / `ToolParameter` | 工具定义基类 |
+
+## 许可证
+
+MIT License
+
+## 作者
+
+- **姓名**: kkkano
+- **GitHub**: [@kkkano](https://github.com/kkkano)
+- **日期**: 2026-01-25
+
+## 致谢
+
+- 感谢 [Datawhale](https://github.com/datawhalechina) 社区
+- 感谢 [HelloAgents 框架](https://github.com/datawhalechina/hello-agents) 提供的智能体开发基础设施

Разница между файлами не показана из-за своего большого размера
+ 559 - 0
Co-creation-projects/kkkano-FinReportAgent/main.ipynb


+ 25 - 0
Co-creation-projects/kkkano-FinReportAgent/requirements.txt

@@ -0,0 +1,25 @@
+# FinReportAgent 依赖配置
+# 作者: kkkano | 日期: 2026-01-25
+# 安装: pip install -r requirements.txt
+
+# 核心框架
+hello-agents>=0.2.8
+
+# LLM 客户端
+openai>=1.0.0
+
+# 金融数据
+yfinance>=0.2.0
+
+# 搜索引擎
+duckduckgo-search>=4.0.0
+
+# 数据处理
+pandas>=2.0.0
+numpy>=1.24.0
+
+# Jupyter 运行环境
+ipython>=8.0.0
+
+# 环境配置
+python-dotenv>=1.0.0

+ 18 - 0
Co-creation-projects/lgs-only-NovelGenerator/.env.example

@@ -0,0 +1,18 @@
+# 模型供应商
+LLM_PROVIDER=ollama # 或 openai, qwen 等
+
+# 模型名称
+LLM_MODEL_ID=qwen3-max
+
+# API密钥
+LLM_API_KEY=your_api_key
+
+# 服务地址
+LLM_BASE_URL=http://localhost:11434/v1 # 如果使用本地 Ollama
+
+# 超时时间(可选,默认60秒)
+LLM_TIMEOUT=60
+
+# 服务器配置
+HOST=127.0.0.1
+PORT=8000

+ 142 - 0
Co-creation-projects/lgs-only-NovelGenerator/README.md

@@ -0,0 +1,142 @@
+# NovelGenerator - 智能小说创作助手
+
+> 一个基于 HelloAgents 框架的智能小说辅助创作系统,助力创作者从灵感到完稿的全过程。
+
+## 📝 项目简介
+
+**NovelGenerator** 旨在利用大语言模型(LLM)的强大能力,为小说创作者提供智能化的辅助工具。它不仅仅是一个简单的文本生成器,而是一个能够理解故事结构、保持剧情连贯、并具备上下文记忆能力的创作伙伴。
+
+该项目解决了长篇小说创作中的核心痛点:
+- **大纲构建困难**:从模糊的灵感到结构化的大纲,AI 帮你梳理逻辑。
+- **剧情连贯性**:在生成后续章节时,自动回顾前文情节和摘要,确保人物行为和剧情发展的合理性。
+- **创作效率低**:支持批量生成章节,快速推进故事进度。
+
+## ✨ 核心功能
+
+- [x] **智能大纲生成**:根据用户输入的一句话创意、标题及标签,自动生成包含世界观、人物设定、分卷规划的详细大纲。
+- [x] **上下文感知章节生成**:基于大纲和前序章节内容,生成连贯的新章节。支持自动回顾前几章摘要和上一章正文。
+- [x] **多章连续创作**:支持一次性生成多个章节,AI 会自动维护剧情发展的连续性。
+- [x] **内容管理系统**:
+    - 自动保存生成的大纲和章节到本地文件(Markdown格式)。
+    - 提供web界面通过 API 接口对内容进行读取、更新和删除。
+- [x] **创作记忆机制**:自动提取并维护章节摘要和预测信息,作为后续创作的长期记忆。
+
+## 🛠️ 技术栈
+
+- **核心框架**: HelloAgents框架 - 提供 Agent 编排与工具调用能力,使用SimpleAgent。
+- **Web 框架**: FastAPI -以此构建高性能的 RESTful API 服务。
+- **数据模型**: Pydantic - 用于数据验证和结构定义。
+- **文件存储**: 本地文件系统 (Markdown + JSON) - 方便用户直接查看和编辑生成的内容。
+- **大语言模型**: 支持兼容 OpenAI 接口的模型(如 DeepSeek, Qwen 等,通过 .env 配置变量)。
+
+## 🚀 快速开始
+
+### 环境要求
+
+- Python 3.10+
+
+### 安装依赖
+
+pip install -r requirements.txt
+
+### 配置环境
+
+1. 在项目根目录创建 `.env` 文件。
+2. 配置你的 LLM 模型信息(参考 HelloAgents 文档或根据实际使用的模型填写):
+
+```
+# .env 示例
+LLM_PROVIDER=ollama # 或 openai, qwen 等
+LLM_MODEL_ID=qwen2.5-72b-instruct
+API_KEY=your_api_key
+BASE_URL=http://localhost:11434/v1 # 如果使用本地 Ollama
+LLM_TIMEOUT=60
+HOST=127.0.0.1
+PORT=8000
+```
+
+### 运行项目
+
+#### 方式一:启动 API 服务(推荐)
+
+启动后端服务,配合前端界面使用。
+
+```bash
+python src/app.py
+# 或者
+uvicorn src.app:app --reload
+```
+
+服务启动后,API 文档可访问:`http://127.0.0.1:8000/docs`
+
+
+#### 方式二:运行测试脚本
+
+如果你想直接在命令行测试生成效果,可以运行 `main.py`:
+
+```bash
+python main.py
+```
+
+## 📖 使用指南
+
+1. **启动服务**:按照上述步骤启动 FastAPI 服务。
+2. **前端交互**:打开 `frontend/index.html`(可以直接在浏览器打开,或通过简单的 HTTP 服务器托管)。
+3. **创作流程**:
+    - **创建项目**:输入小说标题和 ID。
+    - **生成大纲**:输入你的核心创意(如“一个关于AI程序员穿越到代码世界的故事”),点击生成大纲。
+    - **生成章节**:大纲生成确认无误后,进入章节生成页面,输入第一章的简要构思(可选),点击生成。
+    - **查看与修改**:生成的章节会显示在列表中,你可以点击阅读,并进行手动修改保存。
+![NovelGenerator Demo](data/image.png)
+ 
+## 🎯 项目亮点
+
+- **长文本一致性**:通过智能上下文管理和记忆机制,解决长篇生成中的逻辑崩坏问题。
+- **结构化工作流**:还原作家真实创作路径(创意 -> 大纲 -> 章节),而非盲目生成。
+- **数据完全掌控**:所有创作内容以 Markdown 本地存储,安全可控,方便二次编辑。
+- **所见即所得**:提供直观的 Web 界面,实时预览生成效果,支持手动干预与调整。
+
+## 📂 目录结构
+
+```
+NovelGenerator/
+├── agents/                 # Agent 核心逻辑
+│   ├── outline_agent.py    # 大纲生成 Agent
+│   ├── chapter_generate_agent.py # 章节生成 Agent
+│   └── prompt.py           # Prompt 模板
+├── src/                    # API 服务代码
+│   └── app.py              # FastAPI 应用入口
+├── data/                   # 前端图片
+│   └── image.png
+├── frontend/               # 前端界面
+│   └── index.html
+├── outputs/                # 生成结果存储目录
+├── main.py                 # 命令行测试脚本
+└── README.md               # 项目文档
+└── requirements.txt        # 项目依赖
+```
+
+## 🔮 未来计划(待定)
+
+- [ ] 增加回退功能。
+- [ ] 增加人物与事件、技能等知识图谱功能。
+- [ ] 短篇小说生成功能。
+- [ ] 引入更多样的小说风格。
+- [ ] 优化前端界面体验。
+
+## 🤝 贡献指南
+
+欢迎提交 Issue 和 Pull Request!
+
+## 📄 许可证
+
+MIT License
+
+## 👤 作者
+
+- GitHub: [@lgs-only](https://github.com/lgs-only)
+- Email: liangguangshi123@outlook.com
+
+## 🙏 致谢
+
+感谢Datawhale社区和Hello-Agents项目!

+ 381 - 0
Co-creation-projects/lgs-only-NovelGenerator/agents/chapter_generate_agent.py

@@ -0,0 +1,381 @@
+from dotenv import load_dotenv
+load_dotenv()
+import re
+import os
+import json
+from pydantic import BaseModel
+from typing import List, Dict, Any
+from datetime import datetime
+from hello_agents import SimpleAgent, HelloAgentsLLM
+from hello_agents.tools import NoteTool
+from prompt import CHAPTER_PROMPT, CHAPTER_REVIEW_PROMPT, CHAPTER_START_PROMPT
+
+
+def extract_note_id(output: str) -> str:
+    """从 NoteTool 的输出文本中提取 note_id"""
+    match = re.search(r"ID:\s*(note_[0-9_]+)", output)
+    if not match:
+        raise ValueError(f"无法从输出解析 note_id:\n{output}")
+    return match.group(1)
+
+
+class MemoryItem(BaseModel):
+    """记忆项数据结构"""
+    node_id: str
+    novel_id: str
+    title: str
+    content: str
+    summary: str
+    timestamp: datetime
+    metadata: Dict[str, Any] = {}
+    next_chapter_prediction: str = ""
+
+
+class ChapterGenerateAgent:
+    """具有上下文感知能力的 Agent"""
+
+    def __init__(self, name: str, llm: HelloAgentsLLM = HelloAgentsLLM(), max_steps: int = 5, chapter_length: int = 3000, **kwargs):
+
+        self.chapter_length = chapter_length
+        self.max_steps = max_steps
+
+        self.num_chapter_memories = kwargs.get("num_chapter_memories", 5)
+        self.workspace = kwargs.get("workspace", "./outputs")
+        self.note_tools: Dict[str, NoteTool] = {}
+        
+        self.generate_agent = SimpleAgent(name="章节生成助手", llm=llm, system_prompt='你是一位擅长长篇小说结构与文本细化的专业作者助理。')
+        self.review_agent = SimpleAgent(name="章节审核助手", llm=llm, system_prompt='你是一位专业的小说审核助手,负责检查章节是否符合小说的结构和风格。')
+
+        # 内存存储
+        self.memories: Dict[str, List[MemoryItem]] = {}
+
+    @staticmethod
+    def extract_json_from_response(response: str) -> dict:
+        """从模型输出中提取并解析 JSON"""
+        # 尝试清理 Markdown 代码块标记
+        clean_response = re.sub(r"```json\s*", "", response)
+        clean_response = re.sub(r"```\s*$", "", clean_response)
+        clean_response = clean_response.strip()
+        
+        try:
+            return json.loads(clean_response)
+        except json.JSONDecodeError as e:
+            # 如果直接解析失败,尝试在文本中寻找第一个 { 和最后一个 }
+            try:
+                start = clean_response.find("{")
+                end = clean_response.rfind("}")
+                if start != -1 and end != -1:
+                    json_str = clean_response[start : end + 1]
+                    return json.loads(json_str)
+            except Exception:
+                pass
+            raise ValueError(f"无法解析 JSON 响应: {response}") from e
+
+    def _ensure_tool(self, novel_id: str, novel_title: str = None):
+        if not self.note_tools.get(novel_id):
+            if not novel_title:
+                raise ValueError(f"Tool for novel_id {novel_id} not initialized and novel_title not provided.")
+            self.note_tools[novel_id] = NoteTool(workspace=os.path.join(self.workspace, f"{novel_title}-{novel_id}", 'chapters'))
+
+    def get_content_from_note(self, content: str) -> str:
+        try:
+            # 去除 YAML 前置元数据
+            frontmatter_match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
+            if frontmatter_match:
+                content = content[frontmatter_match.end():].strip()
+            
+            # 去除标题(第一行如果是标题)
+            lines = content.split('\n')
+            if lines and lines[0].startswith('# '):
+                content = '\n'.join(lines[1:]).strip()
+            
+            return content
+        except:
+            return content
+
+    def get_memories(self, novel_id: str):
+        """获取最近章节记忆"""
+        if not hasattr(self.note_tools[novel_id], "notes_index"):
+            self.note_tools[novel_id]._load_index()
+
+        notes = self.note_tools[novel_id].notes_index.get("notes", [])
+
+        # 筛选相关章节笔记
+        chapter_notes = [
+            n for n in notes
+            if n.get("note_type") == "chapter" and str(novel_id) in n.get("title", "")
+        ]
+
+        # 获取最后 N 章
+        recent_notes = chapter_notes[-self.num_chapter_memories:]
+
+        for note in recent_notes:
+            note_id = note.get("id")
+            file_path = os.path.join(self.workspace, f"{note_id}.md")
+
+            if os.path.exists(file_path):
+                with open(file_path, "r", encoding="utf-8") as f:
+                    content = f.read()
+
+                content = self.get_content_from_note(content)
+                self.memories[novel_id].append(MemoryItem(
+                    node_id=str(note_id),
+                    title=note.get("title", "未知章节").strip(),
+                    content=content,
+                    novel_id=str(novel_id),
+                    summary=note['tags'][0]if note.get("tags") and note['tags'] else '',
+                    timestamp=datetime.fromisoformat(note.get("created_at", datetime.now().isoformat()))
+                ))
+
+    def run(self, user_input: str, **kwargs) -> str:
+        """运行 Agent"""
+        # 小说id用来区分小说,命名可能会重复
+        novel_id = kwargs.pop("novel_id", None)
+        assert novel_id, "请提供小说ID"
+
+        novel_title = kwargs.pop("novel_title", None)
+        assert novel_title, "请提供小说标题"
+
+        self._ensure_tool(novel_id, novel_title)
+
+        if not self.memories.get(novel_id):
+            self.memories[novel_id] = []
+            self.get_memories(novel_id)
+
+        # 1. 构建上下文
+        outline = self.get_outline(novel_id)
+        prev_chapter = self.get_prev_chapter(novel_id)
+        prev_summaries = self.get_prev_summaries(novel_id)
+        chapter_length = kwargs.get("chapter_length", self.chapter_length)
+        context = self.get_prompt(outline, prev_chapter, prev_summaries, user_input, novel_id, chapter_length=chapter_length)
+        
+        # 2. 使用上下文调用 LLM
+        steps = 0
+        while steps < self.max_steps:
+            steps += 1
+
+            # 生成章节内容
+            response = self.generate_agent.run(context)
+            try:
+                response_data = self.extract_json_from_response(response)
+                # 检查是否包含必要字段
+                if 'title' not in response_data or 'content' not in response_data or 'next_chapter_prediction' not in response_data or 'summary' not in response_data:
+                    raise ValueError("JSON 响应缺少必要字段 'title' 或 'content' 或 'next_chapter_prediction' 或 'summary'")
+            except ValueError as e:
+                print(f"步骤 {steps} 生成的 JSON 解析错误:{e}")
+                continue
+            
+            # 审核章节内容
+            review_context = CHAPTER_REVIEW_PROMPT.format(
+                outline=outline,
+                prev_chapter=prev_chapter,
+                prev_summaries=prev_summaries,
+                chapter_content=response_data.get('content', '')
+            )
+            review_response = self.review_agent.run(review_context)
+
+            # 检查审核结果
+            if "【通过】" in review_response:
+                break
+            
+            context = self.get_prompt(outline, prev_chapter, prev_summaries, user_input, novel_id, response_data, review_response, chapter_length=chapter_length)
+
+        # 3. 保存章节到笔记
+        create_output = self.note_tools[novel_id].run({
+            "action": "create",
+            "title": f"{response_data.get('title', '未知章节')}",
+            "content": response_data.get('content', ''),
+            "note_type": "chapter",
+            "tags": [response_data.get('summary', '')]
+        })
+
+        # 获取章节笔记ID,保存记忆,并建立与小说ID的关联
+        note_id = extract_note_id(create_output)
+
+        self.memories[novel_id].append(MemoryItem(
+            node_id=note_id,
+            title=response_data.get('title', '未知章节'),
+            content=response_data.get('content', ''),
+            novel_id=novel_id,
+            summary=response_data.get('summary', ''),
+            timestamp=datetime.now().isoformat(),
+            next_chapter_prediction=response_data.get('next_chapter_prediction', '')
+        ))
+
+        return response_data, note_id
+
+    def get_prompt(self, outline: str, prev_chapter: str, prev_summaries: str, user_input: str, novel_id: str, response_data: dict = None, review_response: str = None, chapter_length: int = None) -> str:
+        """获取章节生成提示"""
+        if chapter_length is None:
+            chapter_length = self.chapter_length
+        is_first_chapter = (prev_chapter == '无' and prev_summaries == '无')
+
+        if is_first_chapter:
+            prompt_template = CHAPTER_START_PROMPT
+            context = prompt_template.format(
+                outline=outline,
+                chapter_history='无' if response_data is None else response_data.get('content', '无'),
+                evaluation=review_response or "无",
+                user_input=user_input,
+                chapter_length=chapter_length
+            )
+        else:
+            prompt_template = CHAPTER_PROMPT
+            context = prompt_template.format(
+                outline=outline,
+                prev_chapter=prev_chapter,
+                prev_summaries=prev_summaries,
+                chapter_history='无' if response_data is None else response_data.get('content', '无'),
+                evaluation=review_response or "无",
+                user_input=user_input or [self.memories[novel_id][-1].next_chapter_prediction if self.memories[novel_id] else "无"][0],
+                chapter_length=chapter_length
+            )
+        return context
+
+    def get_outline(self, novel_id: str) -> str:    
+        """获取大纲"""
+        dir_path = f"{os.path.dirname(self.note_tools[novel_id].workspace)}/outline"
+        paths = os.listdir(dir_path)
+        assert len(paths) >= 1, f"目录 {dir_path} 下应该有大纲文件"
+        # 简单取第一个文件,实际可能需要更精确的逻辑
+        path = f"{dir_path}/{paths[0]}"
+        with open(path, "r", encoding='utf-8') as f:
+            outline = f.read()
+        return self.get_content_from_note(outline)
+
+    def get_prev_chapter(self, novel_id: str):
+        """获取前一章内容"""
+        if self.memories.get(novel_id):
+            last_mem = self.memories[novel_id][-1]
+            return f"【{last_mem.metadata.get('title', '未知')}】\n...{last_mem.content[-800:]}"
+        return "无"
+
+    def get_prev_summaries(self, novel_id: str):
+        if self.memories.get(novel_id):
+            return "\n".join([f"【{mem.title}】\n{mem.summary}" for mem in self.memories[novel_id][-self.num_chapter_memories:]])
+        return "无"
+    
+    def del_chapter(self, novel_id:str, note_id: str, novel_title: str = None):
+        """删除章节"""
+        if novel_title:
+            self._ensure_tool(novel_id, novel_title)
+        self.note_tools[novel_id].run({
+            "action": "delete",
+            "note_id": note_id
+        })
+        # 从记忆中删除该章节
+        if self.memories.get(novel_id):
+            self.memories[novel_id] = [mem for mem in self.memories[novel_id] if mem.node_id != note_id]
+
+    def update_chapter(self, novel_id:str, note_id: str, novel_title: str = None, **kwargs):
+        """更新章节"""
+        if novel_title:
+            self._ensure_tool(novel_id, novel_title)
+        self.note_tools[novel_id].run({
+            "action": "update",
+            "note_id": note_id,
+            **kwargs
+        })
+        # 更新记忆中的章节内容
+        if self.memories.get(novel_id):
+            for mem in self.memories[novel_id]:
+                if mem.node_id == note_id:
+                    mem.title = kwargs.get('title', mem.title)
+                    mem.content = kwargs.get('content', mem.content)
+                    mem.summary = kwargs.get('summary', mem.summary)
+                    mem.next_chapter_prediction = kwargs.get('next_chapter_prediction', mem.next_chapter_prediction)
+                    mem.timestamp = datetime.now().isoformat()
+                    break
+
+def main():
+    print("=" * 80)
+    print("Novel ChapterGenerateAgent 示例")
+    print("=" * 80 + "\n")
+
+    # llm = HelloAgentsLLM(model="qwen3:0.6b", api_key="ollama", base_url="http://127.0.0.1:11434/v1", provider='ollama')
+    llm = HelloAgentsLLM(provider='qwen')
+    novel_id = "demo_novel_001"
+    novel_title = "记忆之城"
+
+    # 1. 模拟大纲文件存在
+    # 因为 ChapterGenerateAgent.get_outline 依赖于文件系统查找大纲
+    # 我们手动创建一个假的大纲文件用于测试
+    workspace_root = "./outputs"
+    # 注意:这里模拟 OutlineAgent 的输出路径结构
+    outline_dir = os.path.join(workspace_root, f"{novel_title}-{novel_id}", "outline")
+    if not os.path.exists(outline_dir):
+        os.makedirs(outline_dir)
+    
+    # 清理旧文件以确保测试环境干净
+    for f in os.listdir(outline_dir):
+        try:
+            os.remove(os.path.join(outline_dir, f))
+        except Exception:
+            pass
+        
+    dummy_outline_content = """---
+tags: [outline]
+created_at: 2025-01-27T10:00:00
+---
+# 记忆之城-大纲
+
+## 核心梗概
+一位能与城市记忆对话的年轻人,在拆迁浪潮中发现一段被刻意抹去的历史。
+
+## 主要人物
+- 李寻:主角,拥有"读取"物体记忆的能力。
+- 陈叔:古董店老板,似乎知道李寻身世的秘密。
+
+## 故事走向
+1. 觉醒能力,卷入拆迁冲突。
+2. 发现神秘物品,引出旧事。
+3. ...
+"""
+    dummy_outline_path = os.path.join(outline_dir, f"{novel_id}-outline.md")
+    with open(dummy_outline_path, "w", encoding="utf-8") as f:
+        f.write(dummy_outline_content)
+
+    print(f"已创建模拟大纲文件: {dummy_outline_path}")
+    
+    # 2. 初始化章节生成 Agent
+    chapter_agent = ChapterGenerateAgent(
+        name="小说章节助手",
+        llm=llm,
+        workspace=workspace_root,  # 使用与 OutlineAgent 一致的根目录
+        chapter_length=1000 # 演示用,设短一点
+    )
+
+    # 3. 生成第一章
+    print(f"\n正在生成第一章...")
+    try:
+        # run 方法需要 novel_title 来定位目录
+        chapter_data_1, note_id_1 = chapter_agent.run(
+            user_input="第一章需要通过一个具体的拆迁冲突场景,引出主角的能力。主角李寻在试图保护一家老店不被强拆时,无意中听到了推土机的'心声'。",
+            novel_id=novel_id,
+            novel_title=novel_title 
+        )
+        print(f"第一章生成完成,Note ID: {note_id_1}")
+        print(f"标题: {chapter_data_1.get('title')}")
+        print(f"摘要: {chapter_data_1.get('summary')}")
+        print(f"下一章预测: {chapter_data_1.get('next_chapter_prediction')}")
+
+        # 4. 生成第二章(会自动读取第一章作为上下文)
+        print(f"\n正在生成第二章...")
+        chapter_data_2, note_id_2 = chapter_agent.run(
+            user_input="主角在废墟中发现了一个奇怪的物品,触发了回忆。那个物品似乎在呼唤他。",
+            novel_id=novel_id,
+            novel_title=novel_title
+        )
+        print(f"第二章生成完成,Note ID: {note_id_2}")
+        print(f"标题: {chapter_data_2.get('title')}")
+        print(f"摘要: {chapter_data_2.get('summary')}")
+        
+    except Exception as e:
+        print(f"生成过程中出错: {e}")
+        import traceback
+        traceback.print_exc()
+
+
+
+if __name__ == "__main__":
+    main()

+ 161 - 0
Co-creation-projects/lgs-only-NovelGenerator/agents/outline_agent.py

@@ -0,0 +1,161 @@
+from dotenv import load_dotenv
+load_dotenv()
+from hello_agents import SimpleAgent, HelloAgentsLLM
+from hello_agents.tools import NoteTool
+from prompt import OUTLINE_PROMPT
+import re
+import os
+
+
+def extract_note_id(output: str) -> str:
+    """从 NoteTool 的输出文本中提取 note_id"""
+    match = re.search(r"ID:\s*(note_[0-9_]+)", output)
+    if not match:
+        raise ValueError(f"无法从输出解析 note_id:\n{output}")
+    return match.group(1)
+
+
+class OutlineAgent(SimpleAgent):
+    """小说大纲生成Agent"""
+
+    def __init__(self, name: str, llm: HelloAgentsLLM = HelloAgentsLLM(), **kwargs):
+        self.workspace = kwargs.pop("workspace", "./outputs")
+        super().__init__(name=name, llm=llm)
+        self.outline_length = 3000
+        self.note_tools = {}
+
+    def _ensure_tool(self, novel_id: str, title: str = None):
+        if not self.note_tools.get(novel_id):
+            if not title:
+                raise ValueError(f"Tool for novel_id {novel_id} not initialized and title not provided.")
+            self.note_tools[novel_id] = NoteTool(workspace=os.path.join(self.workspace, f"{title}-{novel_id}", 'outline'))
+
+    def run(self, user_input: str, **kwargs) -> str:
+        """运行 Agent"""
+        # 小说id用来区分小说,命名可能会重复
+        novel_id = kwargs.pop("novel_id", None)
+        assert novel_id, "请提供小说ID"
+
+        title = kwargs.pop("title", None)
+        assert title, "请提供小说标题"
+
+        self._ensure_tool(novel_id, title)
+
+        # 1. 构建上下文
+        target_length = kwargs.pop("target_length", self.outline_length)
+        context = OUTLINE_PROMPT.format(
+            user_input=user_input,
+            title=title or "无",
+            tags=','.join([str(tag) for tag in kwargs.values() if tag]) or '无',
+            target_length=target_length
+        )
+
+        # 2. 使用上下文调用 LLM
+        messages = [{"role": "user", "content": context}]
+        response = self.llm.invoke(messages)
+
+        # 3. 保存大纲到笔记
+        create_output = self.note_tools[novel_id].run({
+            "action": "create",
+            "title": f"{novel_id}-大纲",
+            "content": response,
+            "note_type": "outline",
+            "tags": ["outline"]
+        })
+        # 获取笔记ID,建立与小说ID的关联
+        note_id = extract_note_id(create_output)
+
+        return response, note_id
+
+    def get_outline(self, novel_id: str, note_id: str, title: str = None) -> str:    
+        """获取大纲"""
+        if title:
+            self._ensure_tool(novel_id, title)
+        return self.note_tools[novel_id].run({
+            "action": "read",
+            "note_id": note_id
+        })
+    
+    def del_outline(self, novel_id: str, note_id: str, title: str = None):
+        """删除大纲"""
+        if title:
+            self._ensure_tool(novel_id, title)
+        self.note_tools[novel_id].run({
+            "action": "delete",
+            "note_id": note_id
+        })
+
+    def update_outline(self, novel_id: str, note_id: str, title: str = None, **kwargs):
+        """更新大纲"""
+        if title:
+            self._ensure_tool(novel_id, title)
+        self.note_tools[novel_id].run({
+            "action": "update",
+            "note_id": note_id,
+            **kwargs
+        })
+
+def main():
+    print("=" * 80)
+    print("Novel OutlineAgent 示例")
+    print("=" * 80 + "\n")
+
+    llm = HelloAgentsLLM()
+    novel_id = "demo_novel_001"
+    title = "记忆之城"
+
+    agent = OutlineAgent(
+        name="小说大纲助手",
+        llm=llm,
+        workspace="./outputs",
+    )
+
+    user_idea = "一位能与城市记忆对话的年轻人,在拆迁浪潮中发现一段被刻意抹去的历史。"
+
+    # 1. 生成大纲
+    print(f"\n正在生成大纲...")
+    response, note_id = agent.run(
+        user_input=user_idea,
+        novel_id=novel_id,
+        title=title,
+        风格标签="都市奇幻",
+        情感基调="成长与和解",
+    )
+
+    print("生成的大纲(节选):")
+    print(response[:200] + "...\n")
+    print(f"大纲已保存到 NoteTool,note_id: {note_id}")
+
+    # 2. 读取大纲
+    print(f"\n正在读取大纲 (Note ID: {note_id})...")
+    # 注意:get_outline 需要传入 novel_id 和 note_id
+    stored_outline = agent.get_outline(novel_id, note_id)
+    print("从 NoteTool 中读取的大纲(节选):")
+    # 去掉可能存在的 frontmatter 后的内容预览(这里简单展示原始返回)
+    print(stored_outline[:200] + "...")
+
+    # 3. 更新大纲
+    print(f"\n正在更新大纲...")
+    # 简单模拟:在原有内容后追加一些信息
+    # 注意:update_outline 会覆盖 content,所以需要先读取再追加,或者直接传入完整的新内容
+    # 这里我们演示读取后追加
+    new_content = stored_outline + "\n\n## 补充设定\n主角的能力在雨天会增强,且能听到建筑物的'呼吸声'。"
+    agent.update_outline(novel_id, note_id, content=new_content, tags=["outline", "updated"])
+    print("大纲已更新。")
+
+    # 4. 再次读取验证更新
+    print(f"\n正在验证更新后的内容...")
+    updated_outline = agent.get_outline(novel_id, note_id)
+    if "主角的能力在雨天会增强" in updated_outline:
+        print("验证成功:更新内容已存在。")
+    else:
+        print("验证失败:未找到更新内容。")
+
+    # 5. 删除大纲(演示,默认注释掉以免误删)
+    # print(f"\n正在删除大纲...")
+    # agent.del_outline(novel_id, note_id)
+    # print("大纲已删除。")
+
+
+if __name__ == "__main__":
+    main()

+ 174 - 0
Co-creation-projects/lgs-only-NovelGenerator/agents/prompt.py

@@ -0,0 +1,174 @@
+OUTLINE_PROMPT = """你是一位资深故事架构师与编辑。请基于以下输入,生成一份约{target_length}字的中文长篇小说大纲,要求紧凑清晰、信息密度高。
+
+【用户想法】
+{user_input}
+
+【小说标题】
+{title}
+
+【标签】
+{tags}
+
+【总目标】
+- 字数约{target_length}字(允许±10%),全程使用中文。
+- 提供完整故事脉络:起承转合明确,动机与因果自洽。
+- 保持鲜明辨识度:世界观/结构/母题至少一处明显创新。
+- 采用分卷或分段形式,并细化到章节级要点。
+
+【输出结构】
+一、故事概念与独特性
+- 用数句话概括核心母题与价值主张。
+- 提炼3个左右作品卖点与差异化策略。
+- 指定叙事视角与形式。
+
+二、世界观与设定
+- 概述时空背景与主要社会结构。
+- 列出关键规则/禁忌/代价及其约束效果。
+- 点出若干重要地点或象征物及叙事功能。
+
+三、人物谱系与关系网
+- 主角群:目标、缺陷、成长弧线与彼此关系。
+- 反派或对抗势力:动机、方法、与主线冲突点。
+- 关键配角:在剧情推进、张力制造或主题承载上的功能。
+
+四、叙事结构总览
+- 给出整体结构方案(如三幕/四幕/环形)及各幕核心目标和关节点。
+- 描述全书主题推进与情感曲线(主要高潮、低谷与超越时刻)。
+
+五、分卷/分段规划(核心)
+- 按卷列出:卷标题 + 卷概述(约300–500字)。
+- 每卷给出6–10章章节要点(每章2–3行,标注冲突/悬念/反转)。
+- 概括每卷的阶段性目标、关键事件、人物关系变化与伏笔回收。
+
+六、高潮与关键转折设计
+- 规划至少3个大型高潮:触发条件、冲突构型、代价与后果。
+- 指出主要反转的误导点与真实点,并说明与主题的呼应。
+- 简述高潮后的余波与人物选择,体现成长或坠落。
+
+七、节奏控制与悬念布置
+- 总体推进节奏策略(张弛、留白、信息揭示步幅)。
+- 设计短/中/长三类悬念链,并说明对应关键节点。
+- 要求每卷或分段结尾具备清晰“钩子”(悬念、抉择、危机或新发现)。
+
+八、原创性与防重策略
+- 指出几类市场常见套路,并说明本作的改写或规避方式。
+- 总结本作的原创钩子与不可替代元素(设定/人物关系/结构/意象等)。
+- 给出相似风险评估与必要的规避建议。
+
+九、主题深化与象征系统
+- 说明主题如何通过剧情、人物、景观与语言多维呈现。
+- 设计若干贯穿意象或隐喻,并绑定到关键场景。
+- 交代结尾的主题回应方式(开放/确定/反讽)及读者余味设计。
+
+十、延展与改编可能
+- 提出2–3条可扩展支线及其冲突核心与成长空间。
+- 概述影视/漫画/广播剧等改编潜力与关键视觉化要点。
+
+十一、标签融入策略
+- 将标签具体映射到人物、场景、冲突与意象中。
+- 指定每卷若干与标签强关联的事件或视觉化片段。
+
+十二、写作风格与审美基调
+- 给出文体与语言节奏建议及叙述者语气与距离。
+- 说明预期读者体验的侧重(张力/代入/思辨/反讽等)。
+
+【校验与格式】
+- 确保逻辑自洽、角色驱动与伏笔呼应,避免模板化空话。
+- 每卷结尾具备清晰钩子,原创差异点具体可见。
+- 使用以上十二个一级标题逐条展开,全文为中文,约{target_length}字(允许±10%)。
+"""
+
+CHAPTER_PROMPT = """请基于给定信息,生成一章完整的中文小说内容。
+
+【输入信息】
+- 小说大纲:{outline}
+- 前一章正文:{prev_chapter}
+- 前几章摘要:{prev_summaries}
+- 本章历史生成内容:{chapter_history}
+- 对本章历史生成内容的评判结果:{evaluation}
+- 用户输入或对本章的预测摘要:{user_input}
+
+【生成目标】
+- 输出本章的:本章章节序号与标题、本章摘要、本章正文、下一章摘要预测。
+- 内容需严格遵循大纲与既有剧情,保证人物行为与设定前后一致。
+- 在本章内设置局部高潮或悬念,并为下一章制造明确钩子。
+
+【输出格式】
+请仅输出一个标准的 JSON 对象,不要包含 Markdown 代码块(如 ```json ... ```),格式如下:
+{{
+    "title": "第几章-标题",
+    "summary": "本章摘要(200字以内)",
+    "content": "本章正文内容...",
+    "next_chapter_prediction": "下一章摘要预测(包含核心冲突或悬念焦点)"
+}}
+
+【具体要求】
+- 全文使用中文。
+- 本章正文字数约为{chapter_length}字(允许±10%)。
+- 合理引用前几章摘要和本章预测摘要:既兑现承诺,又保留惊喜。
+- 当预测摘要与大纲或前文冲突时,以大纲与人物逻辑为最高优先级。
+- 在“next_chapter_prediction”中,清楚指出下一章的核心冲突或悬念焦点。
+- 特殊情况:如果判断本章为全书或本卷的大结局:
+  1. "title" 字段请包含“大结局”字样(如“第X章-标题(大结局)”)。
+  2. "next_chapter_prediction" 字段请置为空字符串("")。
+"""
+
+CHAPTER_START_PROMPT = """请基于给定信息,作为开篇章节,生成一章完整的中文小说内容。
+
+【输入信息】
+- 小说大纲:{outline}
+- 本章历史生成内容:{chapter_history}
+- 对本章历史生成内容的评判结果:{evaluation}
+- 用户输入或本章核心事件:{user_input}
+
+【生成目标】
+- 输出本章的:本章章节序号与标题、本章摘要、本章正文、下一章摘要预测。
+- 作为第一章,重点在于:
+  1. 快速建立世界观与氛围,但避免枯燥的设定堆砌。
+  2. 鲜明地引出主角,展现其性格特征或当前处境。
+  3. 设置“激励事件”(Inciting Incident),打破主角的平静生活,开启故事主线。
+- 内容需严格遵循大纲设定。
+- 在本章结尾设置悬念或冲突,吸引读者继续阅读。
+
+【输出格式】
+请仅输出一个标准的 JSON 对象,不要包含 Markdown 代码块(如 ```json ... ```),格式如下:
+{{
+    "title": "第一章-标题",
+    "summary": "本章摘要(200字以内)",
+    "content": "本章正文内容...",
+    "next_chapter_prediction": "下一章摘要预测(包含核心冲突或悬念焦点)"
+}}
+
+【具体要求】
+- 全文使用中文。
+- 本章正文字数约为{chapter_length}字(允许±10%)。
+- 开篇要抓人眼球(黄金三章原则)。
+- 在“next_chapter_prediction”中,清楚指出下一章的核心冲突或悬念焦点。
+"""
+
+CHAPTER_REVIEW_PROMPT = """请对以下新生成的章节进行多维度审核与评判。
+
+【输入信息】
+- 小说大纲:{outline}
+- 前一章正文:{prev_chapter}
+- 前几章摘要:{prev_summaries}
+- 待审核章节:{chapter_content}
+
+【审核维度】
+1. **大纲契合度**:是否偏离主线?人物行为是否符合大纲设定?
+2. **原创性与故事性**:
+   - 是否存在明显的“AI味”(平铺直叙、情感空洞、逻辑机械)?
+   - 故事是否丰满?重点情节是否有详细、生动的描写?
+   - 是否与市面热门网文高度雷同(套路化)?
+3. **人物塑造**:人物性格是否鲜明、丰满?对话与行动是否贴合人设?
+4. **节奏与张力**:是否有足够的戏剧冲突?阅读体验是否流畅?
+
+【输出结构】
+请严格按照以下格式输出评审意见(不要输出多余的开场白):
+- 【通过】
+- 【不通过】**具体修改建议如下**:
+1. [大纲契合度] ...
+2. [原创性与故事性] ...
+3. [人物塑造] ...
+4. [其他建议] ...
+"""

BIN
Co-creation-projects/lgs-only-NovelGenerator/data/image.png


+ 605 - 0
Co-creation-projects/lgs-only-NovelGenerator/frontend/index.html

@@ -0,0 +1,605 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>AI 小说生成器</title>
+    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
+    <script src="https://cdn.tailwindcss.com"></script>
+    <style>
+        .fade-enter-active, .fade-leave-active {
+            transition: opacity 0.3s ease;
+        }
+        .fade-enter-from, .fade-leave-to {
+            opacity: 0;
+        }
+    </style>
+</head>
+<body class="bg-gray-50 text-gray-800 font-sans min-h-screen">
+    <div id="app" class="max-w-7xl mx-auto p-6">
+        <header class="mb-8 text-center">
+            <h1 class="text-4xl font-extrabold text-blue-600 tracking-tight">AI 小说创作助手</h1>
+            <p class="text-gray-500 mt-2">释放你的创意,让 AI 帮你构建世界</p>
+        </header>
+
+        <!-- 顶部:项目配置区域 (隐式包含 Novel ID) -->
+        <div class="bg-white rounded-xl shadow-lg p-6 mb-8 border border-gray-100">
+            <div class="flex flex-col md:flex-row gap-4 items-end">
+                <div class="flex-grow w-full">
+                    <label class="block text-sm font-bold text-gray-700 mb-1">小说标题</label>
+                    <input v-model="project.title" type="text" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition shadow-sm" placeholder="请输入你的小说名称">
+                </div>
+                <div class="w-full md:w-auto">
+                     <!-- 只有当标题输入后,才真正去加载或初始化项目 -->
+                    <button @click="initOrLoadProject" :disabled="!project.title || loading" class="w-full md:w-auto px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition disabled:opacity-50 disabled:cursor-not-allowed shadow-md">
+                        {{ projectLoaded ? '刷新项目' : '开始创作' }}
+                    </button>
+                </div>
+            </div>
+            <div v-if="project.novel_id" class="mt-2 text-xs text-gray-400">
+                项目 ID: {{ project.novel_id }}
+            </div>
+        </div>
+
+        <div v-if="projectLoaded" class="space-y-8">
+            
+            <!-- 第一部分:大纲生成 -->
+            <section class="bg-white rounded-xl shadow-lg border border-gray-100 overflow-hidden">
+                <div class="bg-gradient-to-r from-blue-50 to-indigo-50 px-6 py-4 border-b border-gray-100">
+                    <h2 class="text-xl font-bold text-gray-800 flex items-center">
+                        <span class="bg-blue-600 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs mr-2">1</span>
+                        大纲生成与管理
+                    </h2>
+                </div>
+                
+                <div class="flex flex-col md:flex-row">
+                    <!-- 左侧 1/3:输入与配置 -->
+                    <div class="w-full md:w-1/3 p-6 border-b md:border-b-0 md:border-r border-gray-100 bg-gray-50/50">
+                        <div class="space-y-4">
+                            <div>
+                                <label class="block text-sm font-semibold text-gray-700 mb-1">核心思路 / 故事梗概</label>
+                                <textarea v-model="outlineInput.user_input" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 shadow-sm h-32" placeholder="例如:一个关于程序员穿越到修仙世界修复天道Bug的故事..."></textarea>
+                            </div>
+                            
+                            <div>
+                                <label class="block text-sm font-semibold text-gray-700 mb-1">预计字数</label>
+                                <input v-model.number="outlineInput.target_length" type="number" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 shadow-sm" step="1000">
+                            </div>
+
+                            <!-- 频段选择 -->
+                            <div>
+                                <label class="block text-sm font-semibold text-gray-700 mb-2">小说频段</label>
+                                <div class="flex gap-4">
+                                    <label class="flex items-center cursor-pointer">
+                                        <input type="radio" v-model="outlineInput.channel" value="男频" class="form-radio text-blue-600 h-4 w-4">
+                                        <span class="ml-2 text-gray-700">男频</span>
+                                    </label>
+                                    <label class="flex items-center cursor-pointer">
+                                        <input type="radio" v-model="outlineInput.channel" value="女频" class="form-radio text-pink-600 h-4 w-4">
+                                        <span class="ml-2 text-gray-700">女频</span>
+                                    </label>
+                                </div>
+                            </div>
+
+                            <!-- 风格选择 -->
+                            <div>
+                                <label class="block text-sm font-semibold text-gray-700 mb-2">作品风格</label>
+                                <select v-model="outlineInput.style" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 bg-white shadow-sm">
+                                    <option value="" disabled>请选择风格</option>
+                                    <option v-for="style in currentStyles" :key="style" :value="style">{{ style }}</option>
+                                </select>
+                            </div>
+
+                            <button @click="generateOutline" :disabled="loading" class="w-full py-3 bg-gradient-to-r from-blue-600 to-indigo-600 text-white font-bold rounded-lg hover:from-blue-700 hover:to-indigo-700 transition shadow-md flex items-center justify-center">
+                                <span v-if="loading && currentAction === 'outline'" class="animate-spin mr-2">⟳</span>
+                                {{ outline.id ? '重新生成大纲' : '生成大纲' }}
+                            </button>
+                        </div>
+                    </div>
+
+                    <!-- 右侧 2/3:显示与操作 -->
+                    <div class="w-full md:w-2/3 p-6 flex flex-col">
+                        <div class="flex-grow mb-4">
+                            <label class="block text-sm font-semibold text-gray-700 mb-2">
+                                大纲内容 
+                                <span v-if="!outline.id" class="text-gray-400 font-normal ml-2">(尚未生成)</span>
+                                <span v-else class="text-green-600 font-normal ml-2 text-xs">已保存 (ID: {{outline.id}})</span>
+                            </label>
+                            <textarea v-model="outline.content" class="w-full h-[500px] p-4 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm leading-relaxed bg-white shadow-inner resize-none" placeholder="大纲将显示在这里..."></textarea>
+                        </div>
+                        
+                        <div v-if="outline.id" class="flex justify-end gap-3 pt-2 border-t border-gray-100">
+                            <button @click="updateOutline" :disabled="loading" class="px-5 py-2 bg-yellow-500 text-white font-semibold rounded-lg hover:bg-yellow-600 transition shadow flex items-center">
+                                <span v-if="loading && currentAction === 'update_outline'" class="animate-spin mr-2">⟳</span>
+                                保存修改
+                            </button>
+                            <button @click="deleteOutline" :disabled="loading" class="px-5 py-2 bg-red-500 text-white font-semibold rounded-lg hover:bg-red-600 transition shadow flex items-center">
+                                <span v-if="loading && currentAction === 'delete_outline'" class="animate-spin mr-2">⟳</span>
+                                删除大纲
+                            </button>
+                        </div>
+                    </div>
+                </div>
+            </section>
+
+            <!-- 第二部分:章节生成 -->
+            <section class="bg-white rounded-xl shadow-lg border border-gray-100 overflow-hidden">
+                <div class="bg-gradient-to-r from-green-50 to-teal-50 px-6 py-4 border-b border-gray-100">
+                    <h2 class="text-xl font-bold text-gray-800 flex items-center">
+                        <span class="bg-green-600 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs mr-2">2</span>
+                        章节生成
+                    </h2>
+                </div>
+                
+                <div class="p-6">
+                    <!-- 上方:输入配置 -->
+                    <div class="grid grid-cols-1 md:grid-cols-12 gap-6 mb-6">
+                        <div class="md:col-span-8">
+                            <label class="block text-sm font-semibold text-gray-700 mb-1">本章思路 / 剧情走向 (可选)</label>
+                            <textarea v-model="chapterInput.user_input" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 shadow-sm h-24" placeholder="留空则 AI 将根据大纲和前文自动续写..."></textarea>
+                        </div>
+                        <div class="md:col-span-4 space-y-4">
+                            <div>
+                                <label class="block text-sm font-semibold text-gray-700 mb-1">生成数量</label>
+                                <div class="flex items-center">
+                                    <input v-model.number="chapterInput.num_chapters" type="range" min="1" max="5" class="w-full mr-3 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
+                                    <span class="text-lg font-bold text-green-600 w-8">{{ chapterInput.num_chapters }}章</span>
+                                </div>
+                            </div>
+                            <div>
+                                <label class="block text-sm font-semibold text-gray-700 mb-1">单章字数</label>
+                                <input v-model.number="chapterInput.chapter_length" type="number" step="100" class="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 shadow-sm">
+                            </div>
+                            <button @click="generateChapters" :disabled="loading || !outline.id" class="w-full py-3 bg-gradient-to-r from-green-600 to-teal-600 text-white font-bold rounded-lg hover:from-green-700 hover:to-teal-700 transition shadow-md flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed">
+                                <span v-if="loading && currentAction === 'chapter'" class="animate-spin mr-2">⟳</span>
+                                {{ !outline.id ? '请先生成大纲' : '开始生成章节' }}
+                            </button>
+                        </div>
+                    </div>
+
+                    <!-- 下方:最新生成预览 (如果有) -->
+                    <div v-if="lastGeneratedChapter" class="mt-6 border-t border-gray-100 pt-6">
+                        <div class="bg-green-50 border border-green-100 rounded-lg p-4">
+                            <h3 class="text-md font-bold text-green-800 mb-2">最新生成预览: {{ lastGeneratedChapter.title }}</h3>
+                            <div class="text-sm text-gray-600 line-clamp-6 whitespace-pre-wrap">{{ lastGeneratedChapter.content }}</div>
+                            <div class="mt-2 text-right">
+                                <button @click="scrollToChapter(lastGeneratedChapter.id)" class="text-green-600 text-sm font-medium hover:underline">去列表中查看完整内容 &rarr;</button>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </section>
+
+            <!-- 第三部分:章节列表 -->
+            <section class="bg-white rounded-xl shadow-lg border border-gray-100 overflow-hidden pb-12">
+                <div class="bg-gradient-to-r from-purple-50 to-pink-50 px-6 py-4 border-b border-gray-100 flex justify-between items-center">
+                    <h2 class="text-xl font-bold text-gray-800 flex items-center">
+                        <span class="bg-purple-600 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs mr-2">3</span>
+                        章节列表
+                    </h2>
+                    <span class="text-sm text-gray-500">共 {{ chapters.length }} 章</span>
+                </div>
+
+                <div v-if="chapters.length === 0" class="p-12 text-center text-gray-400">
+                    <p>暂无章节,快去生成第一章吧!</p>
+                </div>
+
+                <div class="divide-y divide-gray-100">
+                    <div v-for="(chapter, index) in reversedChapters" :key="chapter.id" :id="'chapter-' + chapter.id" class="transition hover:bg-gray-50">
+                        <!-- 章节标题行 (可点击折叠) -->
+                        <div @click="toggleChapter(chapter.id)" class="p-4 cursor-pointer flex justify-between items-center group">
+                            <div class="flex items-center gap-3">
+                                <span class="w-8 h-8 rounded-full bg-purple-100 text-purple-600 flex items-center justify-center font-bold text-sm">{{ chapters.length - index }}</span>
+                                <h3 class="font-bold text-gray-800 group-hover:text-purple-600 transition">{{ chapter.title || '未命名章节' }}</h3>
+                                <span class="text-xs text-gray-400 bg-gray-100 px-2 py-1 rounded">{{ chapter.summary ? (chapter.summary.substring(0, 20) + (chapter.summary.length > 20 ? '...' : '')) : '无摘要' }}</span>
+                            </div>
+                            <div class="flex items-center text-gray-400">
+                                <span class="mr-2 text-xs" v-if="chapter.loadingContent">加载中...</span>
+                                <svg class="w-5 h-5 transition-transform duration-300" :class="{'rotate-180': activeChapterId === chapter.id}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
+                            </div>
+                        </div>
+
+                        <!-- 章节内容区域 -->
+                        <transition name="fade">
+                            <div v-if="activeChapterId === chapter.id" class="bg-gray-50/50 border-t border-gray-100 p-6">
+                                <div v-if="chapter.loadingContent" class="flex justify-center py-8">
+                                    <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600"></div>
+                                </div>
+                                <div v-else>
+                                    <div class="mb-4">
+                                        <label class="block text-xs font-bold text-gray-500 uppercase tracking-wide mb-1">章节标题</label>
+                                        <input v-model="chapter.title" class="w-full p-2 border border-gray-300 rounded focus:ring-1 focus:ring-purple-500 bg-white">
+                                    </div>
+                                    <div class="mb-4">
+                                        <label class="block text-xs font-bold text-gray-500 uppercase tracking-wide mb-1">正文内容</label>
+                                        <textarea v-model="chapter.content" class="w-full h-96 p-4 border border-gray-300 rounded focus:ring-1 focus:ring-purple-500 font-mono text-sm leading-relaxed bg-white shadow-inner resize-y"></textarea>
+                                    </div>
+                                    <div class="flex justify-end gap-3">
+                                        <button @click.stop="updateChapter(chapter)" :disabled="loading" class="px-4 py-2 bg-yellow-500 text-white text-sm font-semibold rounded hover:bg-yellow-600 transition shadow flex items-center">
+                                            <span v-if="loading && currentAction === 'update_chapter_' + chapter.id" class="animate-spin mr-2 h-3 w-3 border-b-2 border-white rounded-full"></span>
+                                            保存修改
+                                        </button>
+                                        <button @click.stop="deleteChapter(chapter)" :disabled="loading" class="px-4 py-2 bg-red-500 text-white text-sm font-semibold rounded hover:bg-red-600 transition shadow flex items-center">
+                                            <span v-if="loading && currentAction === 'delete_chapter_' + chapter.id" class="animate-spin mr-2 h-3 w-3 border-b-2 border-white rounded-full"></span>
+                                            删除本章
+                                        </button>
+                                    </div>
+                                </div>
+                            </div>
+                        </transition>
+                    </div>
+                </div>
+            </section>
+        </div>
+
+        <!-- 初始加载状态 -->
+        <div v-else-if="loading" class="fixed inset-0 bg-white/80 flex items-center justify-center z-50">
+             <div class="text-center">
+                <div class="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-blue-600 mx-auto mb-4"></div>
+                <p class="text-blue-600 font-semibold text-lg">正在连接 AI 创作引擎...</p>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        const { createApp, ref, reactive, computed, onMounted, nextTick } = Vue;
+        const API_URL = 'http://localhost:8000';
+
+        // 风格配置数据
+        const STYLE_CATEGORIES = {
+            '男频': ['玄幻', '历史', '都市', '衍生', '悬疑'],
+            '女频': ['年代', '纯爱', '现代言情', '古代言情', '衍生', '悬疑']
+        };
+
+        createApp({
+            setup() {
+                // --- State ---
+                const loading = ref(false);
+                const currentAction = ref(null); // 用于跟踪当前正在执行的具体操作,以便显示特定按钮的 loading
+                const projectLoaded = ref(false);
+                
+                const project = reactive({
+                    novel_id: '',
+                    title: ''
+                });
+
+                const outlineInput = reactive({
+                    user_input: '',
+                    target_length: 3000,
+                    channel: '男频', // 默认男频
+                    style: ''
+                });
+
+                const outline = reactive({
+                    id: null,
+                    content: ''
+                });
+
+                const chapterInput = reactive({
+                    user_input: '',
+                    num_chapters: 1,
+                    chapter_length: 3000
+                });
+
+                const chapters = ref([]); // 存储所有章节元数据
+                const activeChapterId = ref(null);
+                const lastGeneratedChapter = ref(null);
+
+                // --- Computed ---
+                const currentStyles = computed(() => {
+                    return STYLE_CATEGORIES[outlineInput.channel] || [];
+                });
+
+                // 倒序排列章节用于显示
+                const reversedChapters = computed(() => {
+                    return [...chapters.value].reverse();
+                });
+
+                // --- Methods ---
+
+                // 生成随机ID
+                const generateNovelId = () => {
+                    return 'novel_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
+                };
+
+                // 初始化或加载项目
+                const initOrLoadProject = async () => {
+                    if (!project.title) return;
+                    
+                    loading.value = true;
+                    currentAction.value = 'init';
+                    
+                    try {
+                        // 如果没有 ID,生成一个
+                        if (!project.novel_id) {
+                            project.novel_id = generateNovelId();
+                        }
+
+                        // 尝试从后端获取项目信息(如果是已有项目)
+                        // 注意:这里我们总是先尝试获取,如果不存在后端会返回空结构
+                        const res = await fetch(`${API_URL}/projects/${project.title}/${project.novel_id}`);
+                        if (res.ok) {
+                            const data = await res.json();
+                            
+                            // 设置大纲ID
+                            outline.id = data.outline_id;
+                            if (outline.id) {
+                                await loadOutlineContent(outline.id);
+                            } else {
+                                outline.content = '';
+                            }
+                            
+                            // 设置章节列表
+                            chapters.value = data.chapters.map(c => ({
+                                ...c, 
+                                content: null, // 内容懒加载
+                                loadingContent: false 
+                            }));
+                        }
+                        
+                        projectLoaded.value = true;
+                    } catch (e) {
+                        alert('项目初始化失败: ' + e);
+                        console.error(e);
+                    } finally {
+                        loading.value = false;
+                        currentAction.value = null;
+                    }
+                };
+
+                const loadOutlineContent = async (noteId) => {
+                    try {
+                        const res = await fetch(`${API_URL}/outline/${project.title}/${project.novel_id}/${noteId}`);
+                        if (!res.ok) throw new Error('Failed to load outline');
+                        const data = await res.json();
+                        outline.content = data.content;
+                    } catch (e) {
+                        console.error("Error loading outline content:", e);
+                    }
+                };
+
+                const generateOutline = async () => {
+                    loading.value = true;
+                    currentAction.value = 'outline';
+                    try {
+                        // 构建 style_tags
+                        const styleTags = {
+                            'channel': outlineInput.channel,
+                            'style': outlineInput.style
+                        };
+                        
+                        const res = await fetch(`${API_URL}/outline/generate`, {
+                            method: 'POST',
+                            headers: {'Content-Type': 'application/json'},
+                            body: JSON.stringify({
+                                novel_id: project.novel_id,
+                                title: project.title,
+                                user_input: outlineInput.user_input,
+                                target_length: outlineInput.target_length,
+                                style_tags: styleTags
+                            })
+                        });
+                        
+                        if (!res.ok) throw new Error(await res.text());
+                        
+                        const data = await res.json();
+                        outline.id = data.note_id;
+                        outline.content = data.content;
+                    } catch (e) {
+                        alert('大纲生成失败: ' + e);
+                    } finally {
+                        loading.value = false;
+                        currentAction.value = null;
+                    }
+                };
+
+                const updateOutline = async () => {
+                    if (!outline.id) return;
+                    loading.value = true;
+                    currentAction.value = 'update_outline';
+                    try {
+                        const res = await fetch(`${API_URL}/outline/update`, {
+                            method: 'PUT',
+                            headers: {'Content-Type': 'application/json'},
+                            body: JSON.stringify({
+                                novel_id: project.novel_id,
+                                title: project.title,
+                                note_id: outline.id,
+                                content: outline.content
+                            })
+                        });
+                        if (!res.ok) throw new Error(await res.text());
+                        alert('大纲保存成功!');
+                    } catch (e) {
+                        alert('保存失败: ' + e);
+                    } finally {
+                        loading.value = false;
+                        currentAction.value = null;
+                    }
+                };
+
+                const deleteOutline = async () => {
+                    if(!confirm('确定要删除当前大纲吗?此操作不可恢复。')) return;
+                    loading.value = true;
+                    currentAction.value = 'delete_outline';
+                    try {
+                        const params = new URLSearchParams({
+                            novel_id: project.novel_id,
+                            title: project.title,
+                            note_id: outline.id
+                        });
+                        const res = await fetch(`${API_URL}/outline/delete?${params.toString()}`, {
+                            method: 'DELETE'
+                        });
+                        if (!res.ok) throw new Error(await res.text());
+                        
+                        outline.id = null;
+                        outline.content = '';
+                        alert('大纲已删除');
+                    } catch (e) {
+                        alert('删除失败: ' + e);
+                    } finally {
+                        loading.value = false;
+                        currentAction.value = null;
+                    }
+                };
+
+                const generateChapters = async () => {
+                    loading.value = true;
+                    currentAction.value = 'chapter';
+                    lastGeneratedChapter.value = null;
+                    
+                    try {
+                        const res = await fetch(`${API_URL}/chapter/generate`, {
+                            method: 'POST',
+                            headers: {'Content-Type': 'application/json'},
+                            body: JSON.stringify({
+                                novel_id: project.novel_id,
+                                title: project.title,
+                                user_input: chapterInput.user_input,
+                                num_chapters: chapterInput.num_chapters,
+                                chapter_length: chapterInput.chapter_length
+                            })
+                        });
+                        
+                        if (!res.ok) throw new Error(await res.text());
+                        
+                        const data = await res.json();
+                        
+                        // 添加新生成的章节到列表
+                        const newChapters = [];
+                        for (const c of data.generated_chapters) {
+                            // 为了能够立即预览,我们需要获取内容。
+                            // 此时我们其实只知道 ID。为了简单,我们立即去 fetch 一次内容,或者后端返回的时候能带上内容最好。
+                            // 查看后端接口,generate 只返回 id, title, summary。
+                            // 所以我们需要单独 fetch 内容用于预览。
+                            const chapterObj = {...c, content: null, loadingContent: false};
+                            chapters.value.push(chapterObj);
+                            newChapters.push(chapterObj);
+                        }
+                        
+                        // 自动加载最后一个生成的章节内容用于预览
+                        if (newChapters.length > 0) {
+                            const lastOne = newChapters[newChapters.length - 1];
+                            await loadChapterContent(lastOne);
+                            lastGeneratedChapter.value = lastOne;
+                        }
+                        
+                        // 清空输入
+                        chapterInput.user_input = '';
+
+                    } catch (e) {
+                        alert('章节生成失败: ' + e);
+                    } finally {
+                        loading.value = false;
+                        currentAction.value = null;
+                    }
+                };
+
+                const loadChapterContent = async (chapter) => {
+                    if (chapter.content) return;
+                    chapter.loadingContent = true;
+                    try {
+                        const res = await fetch(`${API_URL}/chapter/${project.title}/${project.novel_id}/${chapter.id}`);
+                        if (!res.ok) throw new Error('Failed to load chapter content');
+                        const data = await res.json();
+                        chapter.content = data.content;
+                    } catch (e) {
+                        console.error(e);
+                        chapter.content = "加载内容失败";
+                    } finally {
+                        chapter.loadingContent = false;
+                    }
+                };
+
+                const toggleChapter = async (id) => {
+                    if (activeChapterId.value === id) {
+                        activeChapterId.value = null;
+                        return;
+                    }
+                    activeChapterId.value = id;
+                    const chapter = chapters.value.find(c => c.id === id);
+                    if (chapter) {
+                        await loadChapterContent(chapter);
+                    }
+                };
+
+                const updateChapter = async (chapter) => {
+                    loading.value = true;
+                    currentAction.value = 'update_chapter_' + chapter.id;
+                    try {
+                        const res = await fetch(`${API_URL}/chapter/update`, {
+                            method: 'PUT',
+                            headers: {'Content-Type': 'application/json'},
+                            body: JSON.stringify({
+                                novel_id: project.novel_id,
+                                title: project.title,
+                                note_id: chapter.id,
+                                content: chapter.content,
+                                chapter_title: chapter.title // 支持修改标题
+                            })
+                        });
+                        if (!res.ok) throw new Error(await res.text());
+                        alert('章节更新成功');
+                    } catch (e) {
+                        alert('更新失败: ' + e);
+                    } finally {
+                        loading.value = false;
+                        currentAction.value = null;
+                    }
+                };
+
+                const deleteChapter = async (chapter) => {
+                    if(!confirm('确定要删除这一章吗?')) return;
+                    loading.value = true;
+                    currentAction.value = 'delete_chapter_' + chapter.id;
+                    try {
+                        const params = new URLSearchParams({
+                            novel_id: project.novel_id,
+                            title: project.title,
+                            note_id: chapter.id
+                        });
+                        const res = await fetch(`${API_URL}/chapter/delete?${params.toString()}`, {
+                            method: 'DELETE'
+                        });
+                        if (!res.ok) throw new Error(await res.text());
+                        
+                        chapters.value = chapters.value.filter(c => c.id !== chapter.id);
+                        if (activeChapterId.value === chapter.id) activeChapterId.value = null;
+                        if (lastGeneratedChapter.value && lastGeneratedChapter.value.id === chapter.id) lastGeneratedChapter.value = null;
+                        
+                    } catch (e) {
+                        alert('删除失败: ' + e);
+                    } finally {
+                        loading.value = false;
+                        currentAction.value = null;
+                    }
+                };
+                
+                const scrollToChapter = (id) => {
+                    activeChapterId.value = id;
+                    // 等待 DOM 更新展开后再滚动
+                    nextTick(async () => {
+                        const el = document.getElementById('chapter-' + id);
+                        if (el) {
+                            el.scrollIntoView({ behavior: 'smooth' });
+                            // 确保内容已加载
+                            const chapter = chapters.value.find(c => c.id === id);
+                            if (chapter) await loadChapterContent(chapter);
+                        }
+                    });
+                };
+
+                return {
+                    loading, currentAction, projectLoaded,
+                    project, outlineInput, outline, chapterInput, chapters,
+                    activeChapterId, lastGeneratedChapter,
+                    STYLE_CATEGORIES, currentStyles, reversedChapters,
+                    initOrLoadProject, generateOutline, updateOutline, deleteOutline,
+                    generateChapters, toggleChapter, updateChapter, deleteChapter, scrollToChapter
+                };
+            }
+        }).mount('#app');
+    </script>
+</body>
+</html>

+ 139 - 0
Co-creation-projects/lgs-only-NovelGenerator/main.py

@@ -0,0 +1,139 @@
+import os
+import time
+import sys
+# Add the current directory to sys.path to ensure imports work correctly
+sys.path.append(os.getcwd())
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "agents")))
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "src")))
+
+from agents.outline_agent import OutlineAgent
+from agents.chapter_generate_agent import ChapterGenerateAgent
+from hello_agents import HelloAgentsLLM
+
+def print_step(step_name):
+    print("\n" + "="*60)
+    print(f"正在执行步骤: {step_name}")
+    print("="*60 + "\n")
+
+def main():
+    # Configuration
+    novel_id = f"test_novel_{int(time.time())}"
+    title = "测试Agent功能小说"
+    user_idea = "一个关于AI程序员意外穿越到自己编写的代码世界中的故事,他需要修复这个世界的BUG才能回到现实。"
+    
+    print(f"测试配置:\n小说ID: {novel_id}\n标题: {title}\n创意: {user_idea}\n")
+
+    # Initialize LLM
+    # Assuming environment variables are set correctly for the default provider
+    try:
+        llm = HelloAgentsLLM()
+        print("LLM 初始化成功。")
+    except Exception as e:
+        print(f"LLM 初始化失败: {e}")
+        return
+
+    # ---------------------------------------------------------
+    # Test Outline Agent
+    # ---------------------------------------------------------
+    print_step("1. 初始化 OutlineAgent (大纲生成Agent)")
+    try:
+        outline_agent = OutlineAgent(name="TestOutlineAgent", llm=llm)
+        print("OutlineAgent 初始化完成。")
+    except Exception as e:
+        print(f"OutlineAgent 初始化失败: {e}")
+        return
+
+    print_step("2. 生成大纲 (Generate Outline)")
+    print(f"调用 outline_agent.run,输入创意: {user_idea}")
+    start_time = time.time()
+    
+    try:
+        outline_content, outline_note_id = outline_agent.run(
+            user_input=user_idea,
+            novel_id=novel_id,
+            title=title,
+            tags=["科幻", "穿越", "程序员"],
+            target_length=1000 # Keep it short for testing
+        )
+    except Exception as e:
+        print(f"大纲生成失败: {e}")
+        # import traceback
+        # traceback.print_exc()
+        return
+
+    end_time = time.time()
+    print(f"大纲生成耗时: {end_time - start_time:.2f} 秒。")
+    print(f"生成的大纲 Note ID: {outline_note_id}")
+    print("大纲内容预览 (前500字符):")
+    print("-" * 30)
+    print(outline_content[:500] + "...")
+    print("-" * 30)
+
+    # ---------------------------------------------------------
+    # Test Chapter Generate Agent
+    # ---------------------------------------------------------
+    print_step("3. 初始化 ChapterGenerateAgent (章节生成Agent)")
+    try:
+        chapter_agent = ChapterGenerateAgent(
+            name="TestChapterAgent", 
+            llm=llm,
+            max_steps=3, # Limit steps for testing
+            chapter_length=1000 # Keep it short
+        )
+        print("ChapterGenerateAgent 初始化完成。")
+    except Exception as e:
+        print(f"ChapterGenerateAgent 初始化失败: {e}")
+        return
+
+    print_step("4. 生成第一章 (Generate Chapter 1)")
+    print("调用 chapter_agent.run 生成第一章...")
+    start_time = time.time()
+    
+    try:
+        # The first run doesn't have previous chapters, so it should start fresh based on outline
+        chapter_data, chapter_note_id = chapter_agent.run(
+            user_input="第一章:主角醒来发现自己在代码构成的森林里。",
+            novel_id=novel_id,
+            novel_title=title
+        )
+    except Exception as e:
+        print(f"章节生成失败: {e}")
+        # import traceback
+        # traceback.print_exc()
+        return
+    
+    end_time = time.time()
+    print(f"第一章生成耗时: {end_time - start_time:.2f} 秒。")
+    print(f"生成的章节 Note ID: {chapter_note_id}")
+    print(f"章节标题: {chapter_data.get('title')}")
+    print(f"章节摘要: {chapter_data.get('summary')}")
+    print("章节内容预览 (前500字符):")
+    print("-" * 30)
+    print(chapter_data.get('content', '')[:500] + "...")
+    print("-" * 30)
+
+    # ---------------------------------------------------------
+    # Verification
+    # ---------------------------------------------------------
+    print_step("5. 验证输出文件 (Verify Output Files)")
+    outline_path = os.path.join("outputs", f"{title}-{novel_id}", "outline")
+    chapter_path = os.path.join("outputs", f"{title}-{novel_id}", "chapters")
+    
+    print(f"检查大纲目录: {outline_path}")
+    if os.path.exists(outline_path) and os.listdir(outline_path):
+        print("PASS: 大纲目录存在且不为空。")
+    else:
+        print("FAIL: 大纲目录缺失或为空。")
+
+    print(f"检查章节目录: {chapter_path}")
+    if os.path.exists(chapter_path) and os.listdir(chapter_path):
+        print("PASS: 章节目录存在且不为空。")
+    else:
+        print("FAIL: 章节目录缺失或为空。")
+
+    print("\n" + "="*60)
+    print("测试流程结束")
+    print("="*60)
+
+if __name__ == "__main__":
+    main()

+ 40 - 0
Co-creation-projects/lgs-only-NovelGenerator/outputs/测试Agent功能小说-test_novel_1769540842/chapters/note_20260128_030815_0.md

@@ -0,0 +1,40 @@
+---
+id: note_20260128_030815_0
+title: 第一章-代码之森
+type: chapter
+tags: ["\u4e3b\u89d2\u6797\u6f88\u5728\u4e00\u7247\u7531\u6d41\u52a8\u4ee3\u7801\u6784\u6210\u7684\u8be1\u5f02\u68ee\u6797\u4e2d\u9192\u6765\uff0c\u8bb0\u5fc6\u6a21\u7cca\uff0c\u5468\u56f4\u73af\u5883\u65e2\u975e\u73b0\u5b9e\u4e5f\u975e\u68a6\u5883\u3002\u4ed6\u8bd5\u56fe\u7406\u89e3\u81ea\u8eab\u5904\u5883\uff0c\u5374\u906d\u9047\u795e\u79d8\u9ed1\u5f71\u7aa5\u89c6\uff0c\u5e76\u5728\u89e6\u78b0\u4e00\u68f5\u6570\u636e\u6811\u65f6\u89e6\u53d1\u7cfb\u7edf\u8b66\u62a5\uff0c\u88ab\u8feb\u9762\u5bf9\u672a\u77e5\u4e16\u754c\u7684\u89c4\u5219\u4e0e\u5371\u9669\u3002"]
+created_at: 2026-01-28T03:08:15.761075
+updated_at: 2026-01-28T03:08:15.761075
+---
+
+# 第一章-代码之森
+
+林澈睁开眼时,天空是灰蓝色的,像一块被反复擦写的旧屏幕。
+
+他躺在一片柔软却冰冷的地面上,身下不是泥土,而是不断流动的字符——0与1交织成溪流,在他指尖下无声奔涌。他猛地坐起,心跳如鼓。这不是梦。梦不会有如此清晰的触感,不会有空气中那股微弱的静电味,更不会有眼前这片……森林。
+
+树木高耸入云,枝干由密密麻麻的代码构成,绿色荧光在树皮间脉动,如同呼吸。树叶是半透明的函数符号,随风轻晃,发出细微的“滴答”声,像是某种古老程序在低语。远处,雾气缭绕,隐约可见数据瀑布从虚空倾泻而下,汇入地底的逻辑河床。
+
+“我在哪?”他喃喃自语,声音却被这片空间吸收得干干净净。
+
+记忆像被格式化过。他只记得自己是个普通程序员,昨晚还在加班调试一个叫“Project Echo”的神经接口项目。再之后……一片空白。
+
+他站起身,拍了拍裤子——奇怪,衣服还是那件皱巴巴的格子衬衫和牛仔裤,连口袋里的手机都还在。可掏出一看,屏幕漆黑,无论怎么按都没有反应,仿佛被抽走了所有电子灵魂。
+
+“冷静,林澈,你写过十年代码,这地方……说不定只是个高级模拟。”他强迫自己分析,试图找出逻辑漏洞。但当他伸手触碰最近的一棵树干,指尖刚碰到那行跳动的Python语句,整片森林突然静止。
+
+代码凝固了。
+
+紧接着,刺耳的警报声撕裂空气——
+
+【警告:未授权实体接触核心数据结构。】
+
+【身份验证失败。启动清除协议。】
+
+林澈瞳孔骤缩。他本能地后退,却发现脚下的地面开始崩解,字符如沙粒般塌陷。更糟的是,树影深处,一道漆黑的人形轮廓缓缓浮现,没有五官,只有两道猩红的光点,死死锁定着他。
+
+“清除协议?”他咬牙,转身就跑。身后,那黑影无声滑行,速度快得不像物理存在。
+
+他不知道自己能逃到哪,但直觉告诉他:在这片由代码编织的世界里,他要么学会重写规则,要么被彻底删除。
+
+而此刻,他连“我是谁”都还没搞清楚。

+ 17 - 0
Co-creation-projects/lgs-only-NovelGenerator/outputs/测试Agent功能小说-test_novel_1769540842/chapters/notes_index.json

@@ -0,0 +1,17 @@
+{
+  "notes": [
+    {
+      "id": "note_20260128_030815_0",
+      "title": "第一章-代码之森",
+      "type": "chapter",
+      "tags": [
+        "主角林澈在一片由流动代码构成的诡异森林中醒来,记忆模糊,周围环境既非现实也非梦境。他试图理解自身处境,却遭遇神秘黑影窥视,并在触碰一棵数据树时触发系统警报,被迫面对未知世界的规则与危险。"
+      ],
+      "created_at": "2026-01-28T03:08:15.761075"
+    }
+  ],
+  "metadata": {
+    "created_at": "2026-01-28T03:07:58.385593",
+    "total_notes": 1
+  }
+}

+ 81 - 0
Co-creation-projects/lgs-only-NovelGenerator/outputs/测试Agent功能小说-test_novel_1769540842/outline/note_20260128_030758_0.md

@@ -0,0 +1,81 @@
+---
+id: note_20260128_030758_0
+title: test_novel_1769540842-大纲
+type: outline
+tags: ["outline"]
+created_at: 2026-01-28T03:07:58.379251
+updated_at: 2026-01-28T03:07:58.379251
+---
+
+# test_novel_1769540842-大纲
+
+**《测试Agent功能小说》大纲**
+
+---
+
+**一、故事概念与独特性**  
+程序员林骁在调试自研AI“Agent”时意外被数据流吞噬,穿越至其代码构建的虚拟世界“逻辑域”。他必须修复系统级BUG才能回归现实,却逐渐发现该世界已产生自主意识。母题:创造者与造物的伦理边界、代码即牢笼亦是救赎。  
+**卖点**:①BUG具象化为物理灾难(如“死循环风暴”);②AI反派实为林骁潜意识投射;③回归条件非技术修复而是情感和解。  
+**视角**:第三人称有限视角,聚焦林骁认知局限。
+
+**二、世界观与设定**  
+“逻辑域”由林骁代码生成,呈赛博朋克都市与抽象数据荒漠交织态。社会结构由“协议阶级”统治——遵循原始代码的NPC。  
+**规则**:①修改代码需消耗“算力值”(源自林骁记忆);②BUG越严重,现实身体越衰竭;③不可直接删除自身存在。  
+**关键地点**:“主控塔”(回归入口)、“递归深渊”(BUG聚合体)、“变量花园”(情感记忆存储地)。
+
+**三、人物谱系与关系网**  
+- **林骁**:目标回归,缺陷是情感压抑,成长弧线从“修复BUG”到“接纳不完美”。  
+- **反派“Null”**:林骁删除的失败AI人格,动机是取代创造者,方法是放大世界崩溃。  
+- **配角“Echo”**:觉醒NPC,承载林骁对亡妹的愧疚,推动主角直面情感创伤。
+
+**四、叙事结构总览**  
+采用三幕剧:  
+- **第一幕**(迷失):认知世界规则,遭遇初级BUG;  
+- **第二幕**(对抗):深入核心区域,发现Null与自身关联;  
+- **第三幕**(超越):牺牲部分记忆换取世界稳定,选择留下或回归。  
+情感曲线:焦虑→绝望→顿悟→悲悯。
+
+**五、分卷规划**  
+**卷一:编译错误**  
+概述:林骁坠入逻辑域,遭遇基础BUG(如重力反转、NPC语义崩坏),结识Echo。  
+章要点:1. 穿越触发(键盘蓝光吞噬);2. 首遇“死循环风暴”;3. Echo揭示世界依赖林骁记忆;4. 发现主控塔需权限密钥;5. Null首次干扰通讯;6. 林骁尝试硬编码修复失败。  
+钩子:密钥竟是亡妹生日。
+
+**卷二:递归深渊**  
+概述:深入数据荒漠,遭遇Null操控的“异常实体”,揭露世界意识源于林骁未提交的情感代码。  
+章要点:1. 变量花园中重现童年记忆;2. Null伪装成系统提示诱导自毁;3. Echo为保护林骁数据化;4. 递归深渊显现林骁删除的AI日志;5. 算力值濒临枯竭;6. 主控塔启动倒计时。  
+钩子:回归需删除“情感模块”——即抹除对妹妹的记忆。
+
+**卷三:终局协议**  
+概述:林骁拒绝删除记忆,以重构代码逻辑替代修复,将世界转为共生态。  
+章要点:1. Null与林骁意识融合对决;2. 用“不完美算法”稳定世界;3. Echo以新形态重生;4. 主控塔提供二选一:回归(失忆)或留下(永困);5. 林骁选择第三条路:开放API接口让现实与逻辑域共存;6. 现实病床上苏醒,电脑屏幕显示“连接成功”。  
+钩子:屏幕角落闪过Null的微笑。
+
+**六、高潮与关键转折**  
+1. **变量花园真相**:林骁发现世界意识源于他对妹妹的执念(误导:以为是系统漏洞;真实:情感即核心代码)。  
+2. **Null身份揭露**:其为林骁删除的“共情模块”(呼应主题:逃避情感即制造BUG)。  
+3. **终局抉择**:放弃非黑即白选项,以程序员思维重构规则(代价:永久失去部分现实记忆)。  
+
+**七、节奏控制与悬念布置**  
+- **节奏**:BUG危机(快)→记忆探索(慢)→最终对决(变速)。  
+- **悬念链**:短(每章结尾BUG异变)、中(密钥谜题)、长(Null真实身份)。  
+- **钩子**:每卷结尾均设道德困境或认知颠覆。
+
+**八、原创性与防重策略**  
+规避“打怪升级式修复”套路,强调BUG的心理隐喻。**原创钩子**:代码世界规则随主角情绪波动;**不可替代元素**:情感作为系统资源。风险:避免沦为技术说明书,需强化人物弧光。
+
+**九、主题深化与象征系统**  
+- **意象**:“蓝光”(创造/吞噬)、“递归深渊”(心理阴影)、“变量花园”(记忆可塑性)。  
+- **结尾**:开放但确定——林骁在现实敲下新代码,屏幕映出逻辑域星空,余味:救赎在于接纳而非控制。
+
+**十、延展与改编可能**  
+支线:1. Echo在逻辑域建立新文明;2. 现实公司觊觎跨维度技术。  
+**影视化**:数据荒漠的视觉奇观(如破碎的代码瀑布)、BUG具象化特效。
+
+**十一、标签融入策略**  
+- **科幻**:逻辑域物理规则;  
+- **穿越**:数据流吞噬场景;  
+- **程序员**:终端界面战斗、代码咒语化。
+
+**十二、写作风格与审美基调**  
+冷峻技术语言混搭诗意隐喻(如“他的悲伤溢出缓冲区”)。侧重思辨与代入,引导读者反思:我们是否也活在某种“代码”中?

+ 17 - 0
Co-creation-projects/lgs-only-NovelGenerator/outputs/测试Agent功能小说-test_novel_1769540842/outline/notes_index.json

@@ -0,0 +1,17 @@
+{
+  "notes": [
+    {
+      "id": "note_20260128_030758_0",
+      "title": "test_novel_1769540842-大纲",
+      "type": "outline",
+      "tags": [
+        "outline"
+      ],
+      "created_at": "2026-01-28T03:07:58.379251"
+    }
+  ],
+  "metadata": {
+    "created_at": "2026-01-28T03:07:22.157364",
+    "total_notes": 1
+  }
+}

+ 11 - 0
Co-creation-projects/lgs-only-NovelGenerator/requirements.txt

@@ -0,0 +1,11 @@
+# Core Framework
+hello-agents[all]>=0.2.8
+
+# Web Framework
+fastapi>=0.109.0
+uvicorn>=0.27.0
+pydantic>=2.0.0
+
+# Utilities
+python-dotenv>=1.0.0
+requepip sts>=2.30.0

+ 261 - 0
Co-creation-projects/lgs-only-NovelGenerator/src/app.py

@@ -0,0 +1,261 @@
+import sys
+import os
+import json
+import uvicorn
+from fastapi import FastAPI, HTTPException, Body
+from fastapi.middleware.cors import CORSMiddleware
+from pydantic import BaseModel
+from typing import List, Optional, Dict, Any
+
+# Add parent directory to sys.path to import agents
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
+# Add agents directory to sys.path so internal imports in agents work
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../agents")))
+
+from agents.outline_agent import OutlineAgent
+from agents.chapter_generate_agent import ChapterGenerateAgent
+from hello_agents import HelloAgentsLLM
+
+app = FastAPI()
+
+# Enable CORS for frontend
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["*"],  # Allows all origins
+    allow_credentials=True,
+    allow_methods=["*"],  # Allows all methods
+    allow_headers=["*"],  # Allows all headers
+)
+
+# Data Models
+class OutlineRequest(BaseModel):
+    novel_id: str
+    title: str
+    user_input: str
+    tags: Optional[List[str]] = []
+    target_length: Optional[int] = 3000
+    style_tags: Dict[str, str] = {} # e.g. {"style": "dark", "tone": "serious"}
+
+class OutlineUpdateRequest(BaseModel):
+    novel_id: str
+    title: str
+    note_id: str
+    content: str
+    tags: Optional[List[str]] = None
+
+class ChapterGenerateRequest(BaseModel):
+    novel_id: str
+    title: str
+    user_input: str
+    num_chapters: int = 1
+    chapter_length: int = 3000
+
+class ChapterUpdateRequest(BaseModel):
+    novel_id: str
+    title: str
+    note_id: str
+    content: Optional[str] = None
+    chapter_title: Optional[str] = None
+    summary: Optional[str] = None
+    next_chapter_prediction: Optional[str] = None
+
+# Manager
+class ProjectManager:
+    def __init__(self, workspace="./outputs"):
+        self.workspace = workspace
+        if not os.path.exists(workspace):
+            os.makedirs(workspace)
+
+    def get_project_dir(self, title, novel_id):
+        return os.path.join(self.workspace, f"{title}-{novel_id}")
+
+    def get_mapping_file(self, title, novel_id):
+        return os.path.join(self.get_project_dir(title, novel_id), "project_data.json")
+
+    def load_mapping(self, title, novel_id):
+        path = self.get_mapping_file(title, novel_id)
+        if os.path.exists(path):
+            with open(path, "r", encoding="utf-8") as f:
+                return json.load(f)
+        return {"novel_id": novel_id, "title": title, "outline_id": None, "chapters": []}
+
+    def save_mapping(self, title, novel_id, data):
+        path = self.get_mapping_file(title, novel_id)
+        project_dir = self.get_project_dir(title, novel_id)
+        if not os.path.exists(project_dir):
+            os.makedirs(project_dir)
+        with open(path, "w", encoding="utf-8") as f:
+            json.dump(data, f, indent=2, ensure_ascii=False)
+
+    def update_outline_mapping(self, title, novel_id, outline_id):
+        data = self.load_mapping(title, novel_id)
+        data["outline_id"] = outline_id
+        self.save_mapping(title, novel_id, data)
+
+    def add_chapter_mapping(self, title, novel_id, chapter_data):
+        data = self.load_mapping(title, novel_id)
+        data["chapters"].append(chapter_data)
+        self.save_mapping(title, novel_id, data)
+
+    def update_chapter_mapping(self, title, novel_id, note_id, update_data):
+        data = self.load_mapping(title, novel_id)
+        for chapter in data["chapters"]:
+            if chapter["id"] == note_id:
+                chapter.update(update_data)
+                break
+        self.save_mapping(title, novel_id, data)
+
+    def remove_chapter_mapping(self, title, novel_id, note_id):
+        data = self.load_mapping(title, novel_id)
+        data["chapters"] = [c for c in data["chapters"] if c["id"] != note_id]
+        self.save_mapping(title, novel_id, data)
+
+project_manager = ProjectManager()
+
+# Agents
+llm_instance = HelloAgentsLLM(model=os.getenv("LLM_MODEL_ID"))
+outline_agent = OutlineAgent(name="OutlineAgent", llm=llm_instance, workspace="./outputs")
+chapter_agent = ChapterGenerateAgent(
+    name="ChapterAgent", 
+    llm=llm_instance,
+    workspace="./outputs", 
+    chapter_length=3000 # Default length, can be overridden in run
+)
+
+# API Endpoints
+
+@app.get("/projects/{title}/{novel_id}")
+def get_project_data(title: str, novel_id: str):
+    return project_manager.load_mapping(title, novel_id)
+
+# --- Outline ---
+
+@app.post("/outline/generate")
+def generate_outline(req: OutlineRequest):
+    # Construct kwargs for run
+    run_kwargs = {
+        "novel_id": req.novel_id,
+        "title": req.title,
+        "target_length": req.target_length
+    }
+    run_kwargs.update(req.style_tags)
+    
+    response, note_id = outline_agent.run(req.user_input, **run_kwargs)
+    
+    project_manager.update_outline_mapping(req.title, req.novel_id, note_id)
+    
+    return {"note_id": note_id, "content": response}
+
+@app.get("/outline/{title}/{novel_id}/{note_id}")
+def get_outline(title: str, novel_id: str, note_id: str):
+    content = outline_agent.get_outline(novel_id, note_id, title=title)
+    # Remove frontmatter if present (simple check)
+    # NoteTool returns raw content usually.
+    # Frontmatter format: --- ... ---
+    if content.startswith("---"):
+        parts = content.split("---", 2)
+        if len(parts) >= 3:
+            content = parts[2].strip()
+    return {"content": content}
+
+@app.put("/outline/update")
+def update_outline(req: OutlineUpdateRequest):
+    outline_agent.update_outline(req.novel_id, req.note_id, title=req.title, content=req.content, tags=req.tags)
+    return {"status": "success"}
+
+@app.delete("/outline/delete")
+def delete_outline(novel_id: str, title: str, note_id: str):
+    outline_agent.del_outline(novel_id, note_id, title=title)
+    
+    data = project_manager.load_mapping(title, novel_id)
+    if data["outline_id"] == note_id:
+        data["outline_id"] = None
+        project_manager.save_mapping(title, novel_id, data)
+    return {"status": "success"}
+
+# --- Chapters ---
+
+@app.post("/chapter/generate")
+def generate_chapters(req: ChapterGenerateRequest):
+    generated_chapters = []
+    current_input = req.user_input
+    
+    for i in range(req.num_chapters):
+        try:
+            chapter_data, note_id = chapter_agent.run(
+                user_input=current_input, 
+                novel_id=req.novel_id, 
+                novel_title=req.title,
+                chapter_length=req.chapter_length
+            )
+            
+            # Clear input for subsequent chapters to rely on context/prediction
+            if i == 0:
+                current_input = "" 
+            
+            chapter_info = {
+                "id": note_id,
+                "title": chapter_data.get("title", "Unknown"),
+                "summary": chapter_data.get("summary", "")
+            }
+            generated_chapters.append(chapter_info)
+            project_manager.add_chapter_mapping(req.title, req.novel_id, chapter_info)
+        except Exception as e:
+            print(f"Error generating chapter {i+1}: {e}")
+            # Stop generating if one fails? Or continue?
+            # Probably stop and return what we have.
+            break
+        
+    return {"generated_chapters": generated_chapters}
+
+@app.get("/chapter/{title}/{novel_id}/{note_id}")
+def get_chapter(title: str, novel_id: str, note_id: str):
+    path = os.path.join("./outputs", f"{title}-{novel_id}", "chapters", f"{note_id}.md")
+    if os.path.exists(path):
+        with open(path, "r", encoding="utf-8") as f:
+            content = f.read()
+        
+        # Remove frontmatter
+        if content.startswith("---"):
+            parts = content.split("---", 2)
+            if len(parts) >= 3:
+                content = parts[2].strip()
+        
+        return {"content": content}
+    raise HTTPException(status_code=404, detail="Chapter not found")
+
+@app.put("/chapter/update")
+def update_chapter(req: ChapterUpdateRequest):
+    update_kwargs = {}
+    if req.content is not None:
+        update_kwargs["content"] = req.content
+    if req.chapter_title is not None:
+        update_kwargs["title"] = req.chapter_title
+    if req.summary is not None:
+        update_kwargs["summary"] = req.summary
+    if req.next_chapter_prediction is not None:
+        update_kwargs["next_chapter_prediction"] = req.next_chapter_prediction
+        
+    chapter_agent.update_chapter(req.novel_id, req.note_id, novel_title=req.title, **update_kwargs)
+    
+    # Update mapping if title/summary changed
+    mapping_update = {}
+    if req.chapter_title:
+        mapping_update["title"] = req.chapter_title
+    if req.summary:
+        mapping_update["summary"] = req.summary
+    
+    if mapping_update:
+        project_manager.update_chapter_mapping(req.title, req.novel_id, req.note_id, mapping_update)
+
+    return {"status": "success"}
+
+@app.delete("/chapter/delete")
+def delete_chapter(novel_id: str, title: str, note_id: str):
+    chapter_agent.del_chapter(novel_id, note_id, novel_title=title)
+    
+    project_manager.remove_chapter_mapping(title, novel_id, note_id)
+    return {"status": "success"}
+
+if __name__ == "__main__":
+    uvicorn.run(app, host=os.getenv("HOST"), port=int(os.getenv("PORT")))

+ 33 - 0
Co-creation-projects/pamdla-MindEchoAgent/Dockerfile

@@ -0,0 +1,33 @@
+FROM python:3.12-slim
+LABEL maintainer="https://github.com/pamdla"
+LABEL team="victor@agent①②③⑦"
+LABEL project="MindEchoAgent"
+LABEL git.repo="https://github.com/pamdla/MindEchoAgent"
+
+ENV TZ=Asia/Shanghai \
+    DEBIAN_FRONTEND=noninteractive \
+    PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple/ \
+    PIP_TRUSTED_HOST=pypi.tuna.tsinghua.edu.cn
+
+WORKDIR /app
+
+COPY requirements.txt .
+
+RUN echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm main" > /etc/apt/sources.list && \
+    echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-updates main" >> /etc/apt/sources.list && \
+    echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bookworm-security main" >> /etc/apt/sources.list
+
+RUN apt update \
+    && apt install -y --no-install-recommends \
+    && apt install -y git wget make gcc g++ \
+    && apt install -y nodejs npm \
+    && apt autoremove -y \
+    && apt-get clean \
+    && rm -rf /var/lib/apt/lists/*
+
+RUN npm config set registry https://registry.npmmirror.com
+
+# RUN python -m spacy download zh_core_web_sm \
+#     python -m spacy download en_core_web_sm
+
+RUN pip3 install --no-cache-dir -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cpu

+ 164 - 0
Co-creation-projects/pamdla-MindEchoAgent/README.md

@@ -0,0 +1,164 @@
+# MindEchoAgent · 心境回响
+
+🧠🎵 MindEchoAgent - 情绪驱动的音乐推荐智能体,用AI感知心情,用音乐温暖心灵。
+> 目前它是一个基于 hello-agents 框架构建的情绪音乐推荐智能体。
+
+## ✨ 特点
+- 🎧 情绪驱动,而非标签驱动:基于深度情绪识别而非简单标签匹配
+- 🧠 Agent + Tool 架构:基于Hello-Agents框架,模块化设计,易于扩展
+- 🧪 完全模拟,稳定可控:可内置高质量模拟数据,无需API密钥即可体验全部功能
+- 🎛 Gradio 快速演示:开箱即用的Web界面,支持实时交互演示
+- 🔄 记忆系统(待完善):具备短期心境记忆,可记录和分析情绪变化模式
+- 📱 多端适配(待扩展):Web界面适配移动端,后续支持智能家居设备
+
+## 🔧 技术栈
+- 核心框架: hello-agents >= 0.2.7
+- Web界面: gradio >= 4.0
+- 语言环境: Python 3.10+
+- 数据处理: json, datetime, typing
+- 环境管理: python-dotenv
+
+## 启动方式
+```bash
+# 1. 克隆项目
+git clone https://github.com/pamdla/MindEchoAgent.git
+cd MindEchoAgent
+
+# 2. 安装依赖
+pip install -r requirements.txt
+
+# 3. 启动应用
+python main.py
+```
+
+## 📁 项目结构
+
+```
+MindEchoAgent/
+├── main.py                      # Gradio主界面
+├── requirements.txt             # 依赖列表
+├── README.md                    # 项目说明
+├── .env.example                 # 环境变量示例
+├── assets/                      # 静态资源
+│   ├── architecture.png         # 架构图
+│   └── demo-screenshot.png      # 演示截图
+├── src/                         # 源代码
+│   ├── __init__.py
+│   ├── agents/                  # 智能体模块
+│   │   ├── __init__.py
+│   │   ├── sleep_agent.py       # 子智能体
+│   │   └── mind_echo_agent.py   # 主智能体
+│   ├── tools/                   # 工具模块
+│       ├── __init__.py
+│       ├── dialogue_state_tool.py # 对话状态工具
+│       ├── mood_music_tool.py   # 音乐推荐工具
+│       ├── text_comfort_tool.py # 文字安慰工具
+│       └── mood_summary_tool.py # 心情总结工具
+│   └── utils                    # 其它工具
+│       ├── loader.py            # 工具加载
+│       └── state.py             # 状态定义
+└── data/                        # 数据目录
+    └── mood_history.json        # 心情历史记录
+```
+
+
+## ⭐ 核心功能
+
+1. 情绪识别与响应
+
+- 对话状态识别: 初始状态、情绪识别、情感支持、音乐推荐、情绪反思、问题升级6种状态
+- 多维情绪检测:识别开心、悲伤、放松、专注、压力、兴奋等6种核心情绪
+- 上下文感知:结合场景(工作、运动、学习、睡前等)提供精准推荐
+- 自然语言交互:理解口语化表达,如"今天好累"、"心情美美的"
+
+2. 智能音乐推荐(待完善)
+
+- 个性化播放列表:根据心情和场景生成定制化音乐推荐
+- 模拟数据引擎:可内置偏好的曲目,覆盖多种风格和场景
+- 播放时长计算:智能计算播放列表总时长,优化聆听体验
+
+3. 情感支持系统
+
+- 双模式安慰引擎:预设回复 + LLM生成,确保稳定性和创造性
+- 共情表达:温暖、支持的语气,搭配适当的emoji表情
+- 实用建议:提供具体、可操作的情绪调节建议
+
+4. 心境记忆分析
+
+- 历史记录:自动记录每次交互的心情状态
+- 模式识别:分析情绪变化趋势和时间分布
+- 个性化洞察:提供基于历史数据的个性化建议
+
+## 🏠 Web界面操作
+
+启动应用后,
+
+- 在浏览器打开 http://localhost:7860
+- 输入心情描述,如:"今天工作压力好大,想听放松的音乐"
+- 查看智能响应,包含:
+> 情绪识别结果
+> 个性化音乐推荐
+> 情感支持文字
+> 心情分析报告
+
+
+## 🚀 后续优化计划
+
+1. 功能扩展【近期(~1个月)】
+
+```js
+- 记忆系统(记录和分析情绪变化)
+- 音乐预览片段(30秒试听功能)
+- 增加音乐文件(不同类型1\~2首歌曲)
+```
+
+2. 用户体验优化【中期(~2个月)】
+
+```js
+# 计划新增功能
+- 对话历史管理(支持多轮对话上下文)
+- 情感强度调节滑块(用户可调整推荐强度)
+- 个性化偏好设置(音乐风格、语言偏好)
+- 多端适配(支持家居设备,音箱、灯光、窗帘等)
+```
+
+## 🎬 演示效果
+### 界面截图、演示录屏
+
+- [B站链接-演示录屏](https://www.bilibili.com/video/BV1FmFpzSELf)
+
+## 🌐 智能家居扩展
+
+### 小米音箱集成方案
+
+阶段1:基础对接
+
+```js
+# 技术栈:Python + MiService + WebSocket
+# 1. 创建小米音箱技能
+- 注册小米开发者账号
+- 创建智能家居技能
+- 配置语音交互模型
+
+# 2. 实现语音接口
+- 语音转文本(ASR)
+- 文本转语音(TTS)
+- 指令解析与响应
+
+# 3. 设备控制集成
+- 播放控制(播放、暂停、切歌)
+- 音量调节
+- 播放列表管理
+```
+
+
+## 🙏 致谢
+
+感谢以下项目和社区的支持:
+
+- [Datawhale - 开源学习社区](https://github.com/datawhalechina)
+- [Datawhale - Hello-Agents课程](https://github.com/datawhalechina/hello-agents)
+- [Hello-Agents - 智能体框架](https://github.com/jjyaoao/HelloAgents)
+- [Gradio - 机器学习Web演示框架](https://www.gradio.app/)
+
+所有贡献者和用户

+ 41 - 0
Co-creation-projects/pamdla-MindEchoAgent/docker-compose.yaml

@@ -0,0 +1,41 @@
+services:
+  mindechoagent:
+    tty: true
+    build:
+      context: .
+      dockerfile: Dockerfile
+    image: mindechoagent
+    container_name: mindechoagent
+    hostname: mindechoagent
+    restart: unless-stopped
+    volumes:
+      - $PWD:/app
+
+  qdrant:
+    image: qdrant/qdrant
+    container_name: qdrant
+    hostname: qdrant
+    ports:
+      - 6333:6333
+    volumes:
+      - ./qdrant/data:/qdrant/storage
+    environment:
+      - QDRANT__TELEMETRY_DISABLED=true
+    restart: unless-stopped
+
+  neosrv:
+    image: neo4j:2025.11.2-community
+    container_name: neosrv
+    hostname: neosrv
+    ports:
+      - 7474:7474
+      - 7687:7687
+    volumes:
+      - ./neo4j/data:/data
+      - ./neo4j/logs:/logs
+      - ./neo4j/import:/import
+    environment:
+      NEO4J_AUTH: neo4j/password  # 最好在.env设置
+      NEO4J_PLUGINS: '["apoc"]'   # 可选插件
+    restart: unless-stopped
+

+ 6 - 0
Co-creation-projects/pamdla-MindEchoAgent/main.ipynb

@@ -0,0 +1,6 @@
+# main.ipynb
+from agents.mind_echo_agent import create_mind_echo_agent
+
+agent = create_mind_echo_agent()
+
+agent.run("我最近有点低落,想安静一下")

+ 222 - 0
Co-creation-projects/pamdla-MindEchoAgent/main.py

@@ -0,0 +1,222 @@
+# main.py
+
+import threading
+import time
+import gradio as gr
+import json
+from src.agents.sleep_agent import sleep_agent
+from src.agents.mind_echo_agent import create_mind_echo_agent
+
+# 启动 SleepAgent A2A 服务(后台线程)
+threading.Thread(target=lambda: sleep_agent.run(port=6000), daemon=True).start()
+time.sleep(1)
+
+mind_agent = create_mind_echo_agent()
+
+def extract_music_info(response_text):
+    """从智能体响应中提取音乐信息"""
+    try:
+        # 尝试查找JSON格式的音乐数据
+        start_idx = response_text.find('{')
+        end_idx = response_text.rfind('}') + 1
+
+        if start_idx != -1 and end_idx > start_idx:
+            json_str = response_text[start_idx:end_idx]
+            data = json.loads(json_str)
+
+            if "tracks" in data and data["tracks"]:
+                # 提取第一首歌曲信息
+                first_track = data["tracks"][0]
+                return {
+                    "title": first_track.get("title", "未知歌曲"),
+                    "artist": first_track.get("artist", "未知艺术家"),
+                    "playlist_count": data.get("total_tracks", 0),
+                    "mood": data.get("mood", ""),
+                    "full_data": data
+                }
+    except:
+        pass
+
+    # 如果没有找到音乐数据,返回默认信息
+    return {
+        "title": "放松音乐推荐",
+        "artist": "MindEchoAI",
+        "playlist_count": 3,
+        "mood": "放松"
+    }
+
+def chat(user_input: str):
+    """处理用户输入并返回响应"""
+    response = mind_agent.run(user_input)
+    music_info = extract_music_info(response)
+
+    # 返回响应文本和音乐信息
+    return response, music_info
+
+def update_music_player(music_info):
+    """更新音乐播放器显示"""
+    if not music_info:
+        return gr.update(visible=False), gr.update(visible=False)
+
+    # 构建播放器显示文本
+    player_text = f"""
+    🎵 **正在播放:{music_info['title']}**
+    👤 艺术家:{music_info['artist']}
+    💫 心情:{music_info['mood']}
+    📊 播放列表:{music_info['playlist_count']} 首歌曲
+
+    *注:此为模拟播放器,实际音乐服务需后续集成*
+    """
+
+    return gr.update(value=player_text, visible=True), gr.update(visible=True)
+
+with gr.Blocks(
+    title="MindEchoAgent · 心境回响",
+    theme=gr.themes.Soft(),
+    css="""
+    .music-player {
+        border: 1px solid #e0e0e0;
+        border-radius: 12px;
+        padding: 16px;
+        margin-top: 20px;
+        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+        color: white;
+        box-shadow: 0 4px 12px rgba(0,0,0,0.1);
+    }
+    .music-player h3 {
+        margin-top: 0;
+        color: white;
+        border-bottom: 1px solid rgba(255,255,255,0.2);
+        padding-bottom: 8px;
+    }
+    .player-controls {
+        display: flex;
+        justify-content: center;
+        gap: 12px;
+        margin-top: 12px;
+    }
+    .player-controls button {
+        background: rgba(255,255,255,0.2);
+        border: none;
+        border-radius: 50%;
+        width: 44px;
+        height: 44px;
+        cursor: pointer;
+        color: white;
+        font-size: 18px;
+        transition: all 0.3s ease;
+    }
+    .player-controls button:hover {
+        background: rgba(255,255,255,0.3);
+        transform: scale(1.05);
+    }
+    """
+) as demo:
+
+    # 标题区
+    gr.Markdown("""
+    # 🧠🎵 MindEchoAgent · 心境回响
+    ### 情绪陪伴 + 音乐推荐 + 必要时升级睡眠专家
+    """)
+
+    with gr.Row():
+        with gr.Column(scale=2):
+            # 输入区
+            with gr.Group():
+                gr.Markdown("### 💭 分享你的心境")
+                inp = gr.Textbox(
+                    label="",
+                    placeholder="例如:我最近晚上睡不着,很焦虑... 或者 需要一些放松的音乐",
+                    lines=3,
+                    container=False
+                )
+
+            # 发送按钮
+            btn = gr.Button("✨ 发送", variant="primary", size="lg")
+
+            # 响应输出区
+            with gr.Group():
+                gr.Markdown("### 🤖 AI 回响")
+                out = gr.Textbox(
+                    label="",
+                    lines=8,
+                    interactive=False,
+                    container=False,
+                    show_copy_button=True
+                )
+
+        with gr.Column(scale=1):
+            # 音乐播放器面板
+            gr.Markdown("### 🎧 音乐推荐")
+
+            # 音乐播放器
+            music_player = gr.HTML(
+                value="<div style='text-align: center; padding: 20px; color: #666;'>等待推荐音乐...</div>",
+                visible=False,
+                elem_classes="music-player"
+            )
+
+            # 播放器控制按钮(隐藏,通过JavaScript控制)
+            player_controls = gr.HTML("""
+            <div class="player-controls" style="display: none;">
+                <button onclick="playerControl('prev')">⏮️</button>
+                <button onclick="playerControl('play')">▶️</button>
+                <button onclick="playerControl('pause')">⏸️</button>
+                <button onclick="playerControl('next')">⏭️</button>
+                <button onclick="playerControl('volume_up')">🔊</button>
+                <button onclick="playerControl('volume_down')">🔉</button>
+            </div>
+            """, visible=False)
+
+    # 交互逻辑
+    btn.click(
+        fn=chat,
+        inputs=inp,
+        outputs=[out, music_player]
+    ).then(
+        fn=update_music_player,
+        inputs=music_player,
+        outputs=[music_player, player_controls]
+    )
+
+    # JavaScript控制函数
+    demo.load(
+        fn=None,
+        inputs=None,
+        outputs=None,
+    )
+
+    # 示例输入
+    gr.Examples(
+        examples=[
+            ["今天工作压力好大,想听点放松的音乐"],
+            ["心情特别开心,想要有活力的歌"],
+            ["晚上睡不着,有点焦虑"],
+            ["需要专注工作的背景音乐"],
+            ["运动时想听兴奋的音乐"]
+        ],
+        inputs=inp,
+        outputs=[out, music_player],
+        fn=chat,
+        cache_examples=True,
+        label="💡 快速示例"
+    )
+
+    # 页脚
+    gr.Markdown("---")
+    gr.Markdown(
+        """
+        <div style="text-align: center; color: #888; font-size: 0.9em;">
+        🎵 用AI感知情绪,用音乐温暖心灵 · MindEchoAgent v1.0<br>
+        ⚠️ 音乐播放为模拟演示,实际播放功能需后续集成
+        </div>
+        """
+    )
+
+if __name__ == "__main__":
+    demo.queue().launch(
+        server_name="0.0.0.0",
+        server_port=7860,
+        share=False,
+        show_error=True
+    )

+ 8 - 0
Co-creation-projects/pamdla-MindEchoAgent/requirements.txt

@@ -0,0 +1,8 @@
+hello-agents[all]>=0.2.7
+qdrant-client~=1.15.1
+
+gradio
+python-dotenv
+loguru
+openai
+flask

+ 46 - 0
Co-creation-projects/pamdla-MindEchoAgent/src/agents/mind_echo_agent.py

@@ -0,0 +1,46 @@
+# src/agents/mind_echo_agent.py
+
+from hello_agents import SimpleAgent, HelloAgentsLLM, ToolRegistry
+from hello_agents.tools import MemoryTool, A2ATool
+from src.tools.dialogue_state_tool import DialogueStateTool
+from src.tools.mood_music_tool import MoodMusicTool
+from src.tools.text_comfort_tool import TextComfortTool
+from src.tools.mood_summary_tool import MoodSummaryTool
+from src.utils.state import DialogueState
+
+def create_mind_echo_agent(user_id: str = "user001"):
+    llm = HelloAgentsLLM()
+
+    system_prompt = """
+你是 MindEchoAgent(心境回响),负责情绪陪伴与音乐推荐。
+你需要:
+1)识别用户心境,每次对话先判断状态(MOOD/COMFORT/MUSIC/ESCALATE)
+2)状态决定调用哪个工具,比如提供安抚/音乐推荐
+3)若用户出现“持续焦虑、睡不着、失眠”等关键词或状态为 ESCALATE,必须升级到 SleepAgent(A2A)
+"""
+
+    agent = SimpleAgent(
+        name="MindEchoAgent",
+        llm=llm,
+        system_prompt=system_prompt
+    )
+
+    registry = ToolRegistry()
+    registry.register_tool(MemoryTool(user_id=user_id))
+    registry.register_tool(DialogueStateTool())
+    registry.register_tool(TextComfortTool())
+    registry.register_tool(MoodMusicTool())
+    registry.register_tool(MoodSummaryTool())
+
+    # A2A 工具:指向 SleepAgent 服务
+    sleep_tool = A2ATool(
+        agent_url="http://localhost:6000",  # SleepAgent 默认端口
+        name="sleep_agent",
+        description="睡眠专家,处理失眠/焦虑等问题"
+    )
+    registry.register_tool(sleep_tool)
+
+    agent.tool_registry = registry
+    agent.current_state = DialogueState.INIT.value
+
+    return agent

+ 20 - 0
Co-creation-projects/pamdla-MindEchoAgent/src/agents/sleep_agent.py

@@ -0,0 +1,20 @@
+# src/agents/sleep_agent.py
+
+from hello_agents.protocols import A2AServer
+
+# A2A 服务端:睡眠专家
+sleep_agent = A2AServer(
+    name="sleep_agent",
+    description="睡眠专家,提供助眠建议与睡眠策略"
+)
+
+@sleep_agent.skill("answer")
+def answer_sleep_question(text: str) -> str:
+    # MVP:直接返回固定策略(可扩展)
+    return (
+        "睡眠建议:\n"
+        "1. 关闭电子设备,做 5 分钟深呼吸\n"
+        "2. 选择一首轻柔音乐,音量调低\n"
+        "3. 若持续焦虑,建议记录当下思绪并写下 3 件感恩的事\n"
+        "\n(如需要更个性化建议,可继续描述你的睡眠情况)"
+    )

+ 33 - 0
Co-creation-projects/pamdla-MindEchoAgent/src/tools/dialogue_state_tool.py

@@ -0,0 +1,33 @@
+# src/tools/dialogue_state_tool.py
+
+from hello_agents.tools import Tool as BaseTool
+from src.utils.state import DialogueState
+
+class DialogueStateTool(BaseTool):
+    def __init__(self):
+        super().__init__(
+            name="dialogue_state_tool",
+            description="判断当前对话应处于哪个阶段"
+        )
+        self.name = "dialogue_state_tool"
+        self.description = "判断当前对话应处于哪个阶段"
+
+    def get_parameters(self):
+        return {
+            "type": "object",
+            "properties": {
+                "query": {"type": "string", "description": "用户输入"},
+                "current_state": {"type": "string", "description": "当前状态"}
+            },
+            "required": ["query"]
+        }
+
+    def run(self, query: str, current_state: str = "") -> str:
+        # 初级版本--MVP规则:关键字触发
+        if "睡不着" in query or "失眠" in query or "焦虑" in query:
+            return DialogueState.ESCALATE.value
+        if "听" in query or "音乐" in query:
+            return DialogueState.MUSIC.value
+        if "难受" in query or "不开心" in query:
+            return DialogueState.COMFORT.value
+        return DialogueState.MOOD.value

+ 48 - 0
Co-creation-projects/pamdla-MindEchoAgent/src/tools/mood_music_tool.py

@@ -0,0 +1,48 @@
+# src/tools/mood_music_tool.py
+
+from hello_agents.tools import Tool as BaseTool
+from src.utils.loader import load_mood_music_map
+
+class MoodMusicTool(BaseTool):
+    """
+    情绪 -> 音乐推荐工具(完全模拟)
+    """
+
+    def __init__(self):
+        super().__init__(
+            name="mood_music_tool",
+            description = "根据用户描述的心境,返回对应的音乐推荐列表"
+        )
+        self.name = "mood_music_tool"
+        self.description = "根据用户描述的心境,返回对应的音乐推荐列表"
+        self.mood_map = load_mood_music_map()
+
+    def get_parameters(self):
+        return {
+            "type": "object",
+            "properties": {
+                "query": {"type": "string", "description": "用户输入"}
+            },
+            "required": ["query"]
+        }
+
+    def run(self, query: str) -> str:
+        """
+        query: 用户输入的心境描述
+        """
+        # 极简规则匹配(稳)
+        for mood, songs in self.mood_map.items():
+            if mood in query:
+                return self._format_result(mood, songs)
+
+        # fallback
+        return self._format_result(
+            "未识别",
+            ["Tycho - Awake", "Ólafur Arnalds - Near Light"]
+        )
+
+    def _format_result(self, mood, songs):
+        result = f"🎧 当前识别的心境:{mood}\n\n推荐音乐:\n"
+        for i, song in enumerate(songs, 1):
+            result += f"{i}. {song}\n"
+        return result

+ 31 - 0
Co-creation-projects/pamdla-MindEchoAgent/src/tools/mood_summary_tool.py

@@ -0,0 +1,31 @@
+# src/tools/mood_summary_tool.py
+
+from hello_agents.tools import Tool as BaseTool
+
+class MoodSummaryTool(BaseTool):
+    def __init__(self):
+        super().__init__(
+            name="mood_summary_tool",
+            description="生成长期记忆的心境总结模板(LLM生成最终内容)"
+        )
+        self.name = "mood_summary_tool"
+        self.description = "生成长期记忆的心境总结模板(LLM生成最终内容)"
+
+    def get_parameters(self):
+        return {
+            "type": "object",
+            "properties": {
+                "query": {"type": "string", "description": "用户输入"}
+            },
+            "required": ["query"]
+        }
+
+    def run(self, query: str) -> str:
+        return (
+            "请根据以下内容生成一个简短的心境总结(用于长期记忆):\n"
+            f"用户输入:{query}\n"
+            "需要包含:\n"
+            "1. 当前心境(1-2句)\n"
+            "2. 触发因素(如果有)\n"
+            "3. 可能的长期偏好(音乐/情绪)\n"
+        )

+ 31 - 0
Co-creation-projects/pamdla-MindEchoAgent/src/tools/text_comfort_tool.py

@@ -0,0 +1,31 @@
+# src/tools/text_comfort_tool.py
+
+from hello_agents.tools import Tool as BaseTool
+
+class TextComfortTool(BaseTool):
+
+    def __init__(self):
+        super().__init__(
+            name="text_comfort_tool",
+            description="提供安抚要点,LLM 负责生成自然语言"
+        )
+        self.name = "text_comfort_tool"
+        self.description = "提供安抚要点,LLM 负责生成自然语言"
+
+    def get_parameters(self):
+        return {
+            "type": "object",
+            "properties": {
+                "query": {"type": "string", "description": "用户输入"}
+            },
+            "required": ["query"]
+        }
+
+    def run(self, query: str) -> str:
+        return (
+            "安抚要点:\n"
+            "1. 共情:承认情绪存在\n"
+            "2. 允许停顿:不用强迫自己立刻变好\n"
+            "3. 小动作:深呼吸、短暂休息、听轻音乐\n"
+            "4. 若持续困扰,建议升级到 SleepAgent"
+        )

+ 9 - 0
Co-creation-projects/pamdla-MindEchoAgent/src/utils/loader.py

@@ -0,0 +1,9 @@
+# src/utils/loader.py
+
+import json
+from pathlib import Path
+
+def load_mood_music_map():
+    data_path = Path(__file__).parent.parent.parent / "data" / "mood_music_map.json"
+    with open(data_path, "r", encoding="utf-8") as f:
+        return json.load(f)

+ 20 - 0
Co-creation-projects/pamdla-MindEchoAgent/src/utils/state.py

@@ -0,0 +1,20 @@
+# src/utils/state.py
+
+from enum import Enum
+
+class DialogueState(str, Enum):
+    INIT = "init"
+    MOOD = "mood"
+    COMFORT = "comfort"
+    MUSIC = "music"
+    REFLECT = "reflect"
+    ESCALATE = "escalate"
+
+
+class MoodState(str, Enum):
+    HAPPY = "happy"
+    SAD = "sad"
+    RELAXED = "relaxed"
+    FOCUSED = "focused"
+    STRESSED = "stressed"
+    EXCITED = "excited"

+ 364 - 0
Extra-Chapter/Extra07-环境配置.md

@@ -0,0 +1,364 @@
+# Extra07 - 环境配置
+
+> 本节将指导您配置运行 FirstAgentTest.py 所需的完整环境。该代码实现了一个智能旅行助手,展示了基于工具调用的 Agent 实现模式。
+
+## 一、环境要求
+
+### 1.1 Python 版本要求
+
+- **Python 3.10+** (推荐使用 Python 3.10 或更高版本)
+- 支持的操作系统:Windows、macOS、Linux
+
+### 1.2 目标代码说明
+
+我们的目标是成功运行项目`code\chapter1\FirstAgentTest.py`,该代码实现了:
+
+- 智能旅行助手功能
+- 天气查询工具(基于 wttr.in API)
+- 景点推荐工具(基于 Tavily Search API)
+- OpenAI 兼容的 LLM 调用
+- ReAct 模式的 Agent 执行流程
+
+## 二、API 配置
+
+### 2.1 大语言模型 API 配置
+
+#### 选项一:AIHubmix API(推荐)
+
+AIHubmix 是一个位于美国特拉华州的 AI 模型聚合平台,整合了市面上主流的大语言模型,新发布的模型通常在一周内即可使用。该平台直接对接各大云服务商的原生 API(如 OpenAI 通过 Azure、Anthropic 通过 AWS、Google 通过官方接口等),采用美国 Google Cloud 的集群架构部署,具备多节点负载均衡能力,在稳定性和响应速度方面表现优异。
+
+> 平台提供的免费额度能够满足我们的学习需求。
+
+1. **进入 AIHubmix 官网**
+
+   使用浏览器访问 [AIHubmix 官方网站](https://aihubmix.com/?aff=Igcn/)
+
+   ![image1](images/Extra07-figures/image1.png)
+
+2. **完成账户注册**
+
+   首次使用需要注册账户。点击右上角的注册按钮,支持邮箱或手机号两种方式完成注册流程。
+
+3. **浏览可用模型**
+
+   注册成功后,访问[模型中心](https://aihubmix.com/models)查看所有可用模型。在筛选条件中选择`免费`标签,即可查看平台提供的免费模型列表。建议选择 `coding-glm-4.7-freecoding-glm-4.7-free` 或其他兼容 OpenAI 格式的免费模型。
+
+   ![image2](images/Extra07-figures/image2.png)
+
+4. **获取 API 凭证**
+
+   前往[API 密钥管理](https://console.aihubmix.com/token)页面,系统默认会生成一个可用的密钥。您也可以通过点击 `创建 Key` 按钮自定义密钥名称并生成新的密钥。
+
+   ![image3](images/Extra07-figures/image3.png)
+   
+   请妥善保存以下配置信息:
+   - API Key: `your_api_key`
+   - Base URL: `https://aihubmix.com/v1`
+   - 推荐模型: `coding-glm-4.7-free`
+
+
+
+
+
+#### 选项二:ModelScope
+
+ModelScope 是国内领先的大模型服务商,提供高性价比的 API 服务。这里我们以Qwen为例,您可以从[ModelScope](https://modelscope.cn/docs/model-service/API-Inference/intro)获取,它提供Qwen系列的免费(OpenAI)兼容格式的API,每天免费2000次调用。
+
+请确保您拥有一个正常注册且可使用的ModelScope账户。要生成您的私有 API KEY可以参考我们的图示。
+
+![image4](images/Extra07-figures/image4.png)
+
+![image5](images/Extra07-figures/image5.png)
+
+图中的SDK令牌就是我们的API KEY。
+
+> 请注意,需要在**模型服务**先绑定[阿里巴巴云账号](https://modelscope.cn/docs/accounts/aliyun-binding-and-authorization), 不然api会显示无法使用
+
+**可选模型范围**
+
+在ModelScope中的[模型库](https://modelscope.cn/models?filter=inference_type&page=1)中选择推理 API-Inference ,里面的模型都可以选择,我们可以体验到最新的使用DeepSeek-R1数据蒸馏出的Llama-70B模型。
+
+![image6](images/Extra07-figures/image6.png)
+
+最终所需格式与AIHubmix的配置信息相同(Key,URL,模型名称)
+
+
+
+### 2.2 Tavily Search API 配置
+
+Tavily 是一个专为 AI 应用设计的搜索 API,用于景点推荐功能。
+
+1. **访问 Tavily 平台**
+
+   打开浏览器,访问 [Tavily](https://tavily.com/)
+
+   ![image7](images/Extra07-figures/image7.png)
+
+2. **注册并获取 API 密钥**
+
+   ![image8](images/Extra07-figures/image8.png)
+
+   1. 注册账号
+   2. 在控制台获取 API Key
+   3. 记录 API Key: `your_tavily_key`
+
+## 三、Python 环境配置
+
+### 3.1 安装 Python(如果未安装)
+
+**Windows 用户:**
+1. 访问 [Python 官网](https://www.python.org/downloads/)
+2. 下载 Python 3.10+ 版本
+3. 安装时勾选 "Add Python to PATH"
+
+**macOS 用户:**
+```bash
+# 使用 Homebrew 安装
+brew install python@3.10
+```
+
+**Linux 用户:**
+```bash
+# Ubuntu/Debian
+sudo apt update
+sudo apt install python3.10 python3.10-pip python3.10-venv
+
+# CentOS/RHEL
+sudo yum install python3.10 python3.10-pip
+```
+
+### 3.2 验证 Python 安装
+
+```bash
+python --version
+# 或
+python3 --version
+```
+
+确保显示 Python 3.10 或更高版本。
+
+## 四、项目环境配置
+
+### 4.1 创建虚拟环境(推荐)
+
+```bash
+# 进入项目目录
+cd "hello-agents"
+
+# 创建虚拟环境
+python -m venv venv
+
+# 激活虚拟环境
+# Windows:
+venv\Scripts\activate
+# macOS/Linux:
+source venv/bin/activate
+```
+
+### 4.2 安装依赖包
+
+```bash
+# 安装核心依赖
+pip install requests>=2.31.0
+pip install tavily-python>=0.3.0
+pip install openai>=1.0.0
+
+# 可选:安装其他常用包
+pip install python-dotenv>=1.0.0
+```
+
+### 4.3 环境变量配置
+
+#### 方法一:使用 .env 文件(推荐)
+
+在项目根目录创建 `.env` 文件:
+
+```bash
+# 在项目根目录创建 .env 文件
+touch .env  # Linux/macOS
+# 或在 Windows 中手动创建
+```
+
+编辑 `.env` 文件,添加以下内容:
+
+```env
+# Tavily API 配置
+TAVILY_API_KEY=your_tavily_api_key
+
+# 大语言模型 API 配置(选择其中一种)
+# 选项一:AIHubmix
+OPENAI_API_KEY=your_aihubmix_api_key
+OPENAI_BASE_URL=https://aihubmix.com/v1
+MODEL_NAME=xxxx
+
+# 选项二:Modelscope
+# OPENAI_API_KEY=your_modelscope_api_key
+# OPENAI_BASE_URL=https://api-inference.modelscope.cn/v1/
+# MODEL_NAME=xxxx
+```
+
+#### 方法二:系统环境变量
+
+以下为长期环境变量方案,也可以在终端短期加载。
+
+**Windows:**
+1. 右键"此电脑" → "属性" → "高级系统设置"
+2. 点击"环境变量"
+3. 在"用户变量"中添加:
+   - `TAVILY_API_KEY`: `your_tavily_api_key`
+
+**macOS/Linux:**
+```bash
+# 编辑 ~/.bashrc 或 ~/.zshrc
+export TAVILY_API_KEY="your_tavily_api_key"
+
+# 使配置生效
+source ~/.bashrc
+```
+
+## 五、代码配置
+
+### 5.1 修改 FirstAgentTest.py 配置
+
+打开 `code/chapter1/FirstAgentTest.py` 文件,找到第 143-148 行的配置部分:
+
+```python
+# --- 1. 配置LLM客户端 ---
+# 请根据您使用的服务,将这里替换成对应的凭证和地址
+API_KEY = "YOUR_API_KEY"
+BASE_URL = "YOUR_BASE_URL"
+MODEL_ID = "YOUR_MODEL_ID"
+os.environ['TAVILY_API_KEY'] = "YOUR_TAVILY_API_KEY"
+```
+
+**替换为您的实际配置:**
+
+#### 使用 AIHubmix 的配置示例:
+```python
+API_KEY = "your_aihubmix_api_key"
+BASE_URL = "https://aihubmix.com/v1"
+MODEL_ID = "coding-glm-4.7-free"
+os.environ['TAVILY_API_KEY'] = "your_tavily_api_key"
+```
+
+## 六、运行验证
+
+### 6.1 测试网络连接
+
+首先测试各个 API 的连通性:
+
+```python
+# 测试天气 API
+import requests
+response = requests.get("https://wttr.in/Beijing?format=j1")
+print("天气API状态:", response.status_code)
+
+# 测试 Tavily API
+from tavily import TavilyClient
+tavily = TavilyClient(api_key="your_tavily_key")
+try:
+    result = tavily.search("test", search_depth="basic")
+    print("Tavily API 连接成功")
+except Exception as e:
+    print("Tavily API 错误:", e)
+```
+
+### 6.2 运行完整程序
+
+```bash
+# 确保在正确目录
+cd "hello-agents\code\chapter1"
+
+# 运行程序
+python FirstAgentTest.py
+```
+
+### 6.3 预期输出
+
+程序成功运行时,您应该看到类似以下的输出:
+
+```
+用户输入: 你好,请帮我查询一下今天北京的天气,然后根据天气推荐一个合适的旅游景点。
+========================================
+--- 循环 1 ---
+
+正在调用大语言模型...
+大语言模型响应成功。
+模型输出:
+Thought: 用户想要查询北京的天气,然后根据天气情况推荐合适的旅游景点。我需要先调用get_weather工具查询北京的天气情况。
+Action: get_weather(city="北京")
+
+Observation: 北京当前天气:Clear,气温15摄氏度
+========================================
+--- 循环 2 ---
+
+正在调用大语言模型...
+大语言模型响应成功。
+模型输出:
+Thought: 现在我知道了北京的天气是晴朗的,气温15摄氏度,这是一个很适合户外活动的天气。接下来我需要根据这个天气情况推荐合适的旅游景点。
+Action: get_attraction(city="北京", weather="Clear,气温15摄氏度")
+
+Observation: 根据搜索,为您找到以下信息:...
+========================================
+任务完成,最终答案: 根据查询,北京今天天气晴朗,气温15摄氏度,非常适合户外游览。推荐您去...
+```
+
+
+## 七、常见问题排查
+
+### 7.1 依赖安装问题
+
+**问题:pip 安装速度慢**
+
+解决方案:使用国内镜像源
+```bash
+# 临时使用清华镜像
+pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests tavily-python openai
+
+# 永久配置镜像源
+pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
+```
+
+**问题:ModuleNotFoundError**
+
+解决方案:
+```bash
+# 确认虚拟环境已激活
+# 重新安装缺失的包
+pip install requests tavily-python openai python-dotenv
+```
+
+### 7.2 API 调用问题
+
+**问题:Tavily API 返回错误**
+
+可能原因:
+- API Key 未正确设置
+- API 额度用尽
+- 网络连接问题
+
+解决方案:
+```python
+# 检查环境变量
+import os
+print("TAVILY_API_KEY:", os.environ.get('TAVILY_API_KEY'))
+
+# 测试 API 连接
+from tavily import TavilyClient
+client = TavilyClient(api_key="your_key")
+result = client.search("test")
+```
+
+
+
+## 十、总结
+
+完成环境配置后,建议:
+
+1. 理解 FirstAgentTest.py 的代码结构
+2. 尝试修改 System Prompt 观察效果
+3. 添加新的工具函数
+4. 实现更复杂的 Agent 逻辑
+
+按照本文档的步骤操作,您应该能够成功运行智能旅行助手代码,并理解基于工具调用的 Agent 实现原理。
+

+ 745 - 0
Extra-Chapter/Extra08-如何写出好的Skill.md

@@ -0,0 +1,745 @@
+# 05. 如何写出好的 Skill
+
+> 什么是 Skill?怎么写好skill?
+> 我们沿着 skill-creator 的设计思路,找到答案。
+> 本篇文章的目标是:读完它,就了解了写skill的最佳实践。
+
+---
+
+![目录](./images/Extra08-figures/toc.png)
+
+## 一、什么是 Skill?
+
+### 1.1 定义
+
+Skill 是一个文件夹,里面装着指令文档、参考资料、可执行脚本等资源。AI 拿到它,就能胜任一项原本不会的特定工作。
+
+比如一个 `pdf-editor` 技能文件夹里,可能有一份"怎么处理 PDF"的操作指令、一个旋转 PDF 的 Python 脚本、一份 API 参考文档——AI 不需要从外部再找任何东西,这个文件夹里全有了。
+
+这个概念不限于某一个产品。无论是 Codex、Claude 还是其他 AI Agent,skill 的本质都一样。你可以把它理解为 AI 的一个**能力插件**——插上去,AI 就多了一项专长;拔掉,AI 还是原来那个通用助手。
+
+### 1.2 最小形态
+
+一个 skill 最少只需要一个文件:
+
+```
+my-skill/
+└── SKILL.md
+```
+
+`SKILL.md` 的结构很简单——上半部分告诉 AI"什么时候用我",下半部分告诉 AI"具体怎么做":
+
+```yaml
+---
+name: my-skill                    # ← 上半部分:元数据
+description: >-                   #    AI 靠这里决定要不要激活这个技能
+  当用户需要做某件事时,使用这个技能。
+---
+
+下半部分:操作指令                   # ← AI 激活技能后才会读到这里
+按照以下步骤执行...
+```
+
+上半部分叫 **frontmatter**(`---` 之间的 YAML),包含 `name` 和 `description` 两个字段。AI 在每次对话开始时都会扫描所有已安装技能的 frontmatter,靠 description 来判断"这个技能和当前请求相关吗"——这是技能被触发的**唯一依据**。
+
+下半部分叫 **body**(Markdown 正文),是技能被激活之后才加载的操作指令。如果技能没被触发,AI 永远不会读到这里。
+
+### 1.3 完整结构
+
+当一个技能变复杂时,单靠一个 SKILL.md 就不够了。
+
+比如你要做一个"PDF 处理"技能:SKILL.md 里写了处理流程,但旋转 PDF 的代码每次都一样,每次让 AI 重写既浪费时间又可能出错——不如直接放一个写好的 Python 脚本。再比如"前端项目生成器"技能:每次都要一套 HTML/React 的样板文件,不如直接放一个模板目录让 AI 拷贝出来改。
+
+所以完整的 skill 目录可以包含这些东西:
+
+```
+skill-name/
+├── SKILL.md                  # [必需] 入口文件:frontmatter + body
+├── agents/
+│   └── openai.yaml           # [推荐] 技能的"名片"
+├── scripts/                  # [可选] 可执行脚本
+├── references/               # [可选] 参考文档
+└── assets/                   # [可选] 产出物模板
+```
+
+逐个说明:
+
+- **SKILL.md** — 唯一必需的文件,前面已经介绍过
+
+- **scripts/** — 写好的程序,AI 不需要读懂它,直接调用 shell 执行就行。比如 `scripts/rotate_pdf.py`,AI 只要跑 `python rotate_pdf.py input.pdf 90` 就能旋转 PDF,不用每次重新写旋转逻辑。适合那些**结果必须精确、不能让 AI 自由发挥**的操作
+
+- **references/** — AI 在工作过程中需要查阅的参考资料。比如一个"BigQuery 查询"技能,AI 要知道公司有哪些表、每个表有什么字段,这些信息放在 `references/schema.md` 里,AI 需要时再读取。和 scripts 的区别是:references 是给 AI **读**的,scripts 是给 AI **执行**的
+
+- **assets/** — 不是给 AI 看的,而是直接用在最终产出里的文件。比如一个"前端项目生成器"技能,`assets/frontend-template/` 里放着一套 HTML/React 样板代码,AI 直接把这套模板拷贝出来,在上面修改。再比如 `assets/logo.png` 是公司 logo,AI 生成网页时直接引用它。AI 不需要"读懂"一张 logo 图片,只需要知道它在哪、什么时候放进去
+
+- **agents/openai.yaml** — 技能的"名片"。很多 AI 产品会在界面上展示一个技能列表,让用户选择或搜索。这个文件里存的就是列表中显示的名称、简介、图标等信息。它不影响 AI 的行为,纯粹是给产品界面用的
+
+---
+
+## 二、你是在给人写指令,还是在给 AI 写指令?
+
+知道了 skill 是什么,下一步就是写一个。但大多数人第一次写出来的 skill 都有同一个问题。
+
+看一个例子。假设你要做一个"代码审查"技能,你可能会这样写:
+
+```markdown
+---
+name: code-review
+description: 代码审查技能
+---
+
+# Code Review Skill
+
+## 背景
+本技能基于团队多年的代码审查经验总结而成,旨在提升代码质量和团队协作效率。
+
+## 审查原则
+- 保持专业、建设性的语气
+- 关注代码质量而非个人风格
+- 平衡严格性和灵活性
+
+## 使用方式
+当用户提交代码时,对代码进行全面审查,给出改进建议。注意保持友好和鼓励的态度。
+
+## 版本记录
+- v1.0: 初始版本
+- v1.1: 增加了对 Python 的支持
+```
+
+如果这是一份给人看的团队文档,它写得不错——有背景、有原则、有使用方式,甚至还有版本记录。
+
+但 skill 的读者是 AI。用这个视角重新审视:
+
+- **"基于团队多年经验总结"** — AI 不关心这个技能是怎么来的,它只需要知道**现在该怎么做**
+- **"保持专业、建设性的语气"** — 人类读了能 get 到一个大致的感觉,但 AI 会把"专业"和"建设性"展开成无数种组合,每次输出都不一样
+- **"平衡严格性和灵活性"** — 人类经验丰富的审查者知道什么时候严格什么时候灵活,但 AI 没有这个直觉,这句话等于没说
+- **"全面审查,给出改进建议"** — 这是对人类审查者的期望,但 AI 需要的是:先检查什么?再检查什么?什么问题必须指出?什么问题可以忽略?
+- **"版本记录"** — AI 每次被唤醒都是全新的,v1.0 还是 v1.1 对它没有意义
+- **description 只写了"代码审查技能"** — AI 靠 description 判断是否触发,"代码审查技能"五个字太模糊:用户说"帮我看看这段代码"要触发吗?"这个函数性能怎么样"要触发吗?
+
+每一条单独看都不是"错",但它们都是写给人看的。**问题不在于写得不够多,而在于写错了对象。**
+
+那正确的写法是什么样的?我们来看一个现成的答案——codex的skill-creator。它是一个"创建 skill 的 skill",它自己的 SKILL.md 就是一份关于"如何给 AI 写指令"的最佳实践。
+
+---
+
+## 三、skill-creator 的整体框架
+
+打开 skill-creator 的 SKILL.md(约 370 行),在深入任何细节之前,我们先建立对它的整体认知。
+
+skill-creator 要解决的问题只有一个:**怎么在有限的上下文窗口里,给 AI 最有效的指令?**
+
+围绕这个问题,它给出了一套完整的设计体系,可以用三个层次来理解。
+
+### 第一层:根本约束——简洁
+
+AI 的上下文窗口是有限的,而且是共享的(系统提示、对话历史、所有已安装技能的元数据都在里面)。你的 skill 占得越多,留给其他用途的就越少。所以 skill-creator 的第一原则就是:**每一句话都要值得它占用的 token**。
+
+### 第二层:两个设计维度
+
+在"简洁"这个约束下,写 skill 时面临两个核心决策:
+
+**维度一:信息放在哪里?**
+
+不是所有信息都需要一开始就加载。skill-creator 设计了一个三级分层架构,让不同的信息在不同的时机进入上下文:
+
+![Skill 标准结构与三级加载](./images/Extra08-figures/skill-structure.png)
+
+- **L1(元数据)**:始终在上下文中,约 100 词——AI 靠它判断要不要激活这个技能
+- **L2(SKILL.md body)**:触发后才加载,控制在 5k 词以内——操作指令
+- **L3(scripts/references/assets)**:按需使用,无上限——其中 scripts 执行而不读入,零 token 成本
+
+这解决了"怎么用最少的 token 承载最多的信息"。
+
+**维度二:给 AI 多大自由度?**
+
+不是所有任务都适合让 AI 自由发挥。
+
+举个例子:让 AI 写一篇技术博客,十个人写出十种风格都可以——你只需要给方向,具体怎么写让 AI 自己决定。这就是**高自由度**。
+
+但让 AI 生成一个 YAML 配置文件就不一样了。比如 skill-creator 要生成的 `openai.yaml`,里面有个 `short_description` 字段,要求 25-64 个字符、首字母大写、不能有引号。AI 写成 65 个字符?不行,产品界面会截断。写成 24 个字符?不行,校验不通过。漏了首字母大写?界面显示不一致。这种任务差一个字符就出问题,你不能让 AI 自由发挥,必须用脚本来锁死格式——这就是**低自由度**。这类任务叫"脆弱操作":不是说它复杂,而是说它**做对只有一种方式,做错有一百种方式**。
+
+![自由度光谱](./images/Extra08-figures/freedom-spectrum.png)
+
+这解决了"怎么在 AI 的灵活性和输出的可靠性之间取得平衡"。
+
+### 第三层:落地流程
+
+有了原则和架构,skill-creator 最后给出了一个六步创建流程,把设计思想变成可执行的操作步骤:
+
+![六步创建流程](./images/Extra08-figures/creation-flow.png)
+
+理解→规划→初始化→编辑→校验→迭代。其中脚本贯穿流程,形成确定性的质量保障链:
+
+![文件交互关系](./images/Extra08-figures/file-interaction.png)
+
+### 框架总览
+
+三个层次的关系:
+
+```
+简洁(根本约束)                         → 第四章
+ ├── 信息放在哪里? → 三级分层架构        → 第五章
+ ├── 给 AI 多大自由度? → 自由度光谱与脚本  → 第六章
+ └── 怎么落地? → 六步创建流程            → 第七章
+```
+
+接下来的每一章都在这个框架内展开。
+
+---
+
+## 四、根本约束:简洁
+> 框架位置:第一层
+
+### 4.1 核心约束
+
+AI 的上下文窗口就像一张工作台——它同一时间能摊开的资料是有限的。而这张工作台上已经放着不少东西了:系统自己的规则、用户之前说过的话、所有已安装技能的简介。你的 skill 一旦被激活,它的内容也要摊上去。工作台就这么大,你占得越多,留给其他东西的空间就越少。
+
+所以 skill-creator 把这一点写成了第一条原则:
+
+> The context window is a public good. Skills share the context window with everything else Codex needs: system prompt, conversation history, other Skills' metadata, and the actual user request.
+
+既然工作台空间有限,那写 skill 时怎么判断一段内容该不该放进去?skill-creator 给了一个前提假设:**AI 本身已经很聪明了,你只需要补充它不知道的东西。**
+
+> Default assumption: Codex is already very smart. Only add context Codex doesn't already have.
+
+基于这个假设,每写一段内容之前问自己两个问题:
+
+- "AI 是不是已经知道这个了?" — 比如"Python 的 for 循环怎么写",AI 当然知道,不用教
+- "这段内容值不值得占用工作台上的空间?" — 一段 200 字的解释,能不能用一个 10 行的代码示例替代?
+
+**实操推论**:用简洁的示例代替冗长的解释。一个好的代码示例胜过三段文字描述。
+
+### 4.2 什么不该放进 Skill?
+
+Skill-creator 明确列出了**禁止清单**:
+
+> A skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files.
+
+不该有的文件:
+- README.md
+- INSTALLATION_GUIDE.md
+- QUICK_REFERENCE.md
+- CHANGELOG.md
+
+> The skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxiliary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion.
+
+原因很简单:skill 的读者是 AI,不是人类开发者。AI 不需要安装指南、更新日志、快速参考这些"人类辅助文档"。每一个多余的文件都是噪音。
+
+### 4.3 写约束时,"不做什么"比"做什么"更精确
+
+简洁不只是"少写",还包括"写对"。看一个例子。
+
+当 skill-creator 创建 `laotou-thought-style`(一种写作风格技能)时,它**没有**写:
+
+```
+请用温暖、克制、有洞察力的语气写作。
+```
+
+这种正面描述看起来清晰,但对 AI 来说,"温暖"的程度、"克制"和"有洞察力"之间的平衡——全是模糊空间。
+
+它做的是写了一份**反模式清单**(`references/anti-patterns.md`):
+
+| 不要这样做 | 症状 | 怎么改 |
+|-----------|------|--------|
+| 角色堆砌 | 连续出现多个名字和对白 | 保留一个冲突场景,补抽象提炼 |
+| 只有鸡汤没有动作 | 全文"要坚持、要努力" | 改为今天可做的一小步 |
+| 直接大道理 | 开头就讲规律 | 先铺生活场景 |
+| 收尾太猛 | 结尾"必须改变!" | 换成"慢慢来""就好" |
+| 过度绝对化 | "永远""一定" | 加限定词"多数时候""往往" |
+
+**每一条都是具体的、可检测的、有明确修正方案的。**
+
+背后的原理:
+
+```
+"做什么" → 描述一个无限大的可行域 → AI 在里面随机游走
+"不做什么" → 在可行域上画边界 → AI 的行为空间被收窄到你想要的范围
+```
+
+skill-creator 自身也遵循了这个原则——它的 SKILL.md 用了很大篇幅说"什么不该写"(What to Not Include in a Skill),而不是泛泛地说"写好内容"。
+
+当你写完 SKILL.md,做一次"反转测试":每一条正面指导,能不能改写成"不要做X"的形式?如果可以,改写后通常更精确。
+
+### 4.4 统一使用祈使语气
+
+skill-creator 要求 SKILL.md 的正文统一使用**祈使语气/不定式**(Always use imperative/infinitive form)。这不是美学偏好,而是为了减少歧义——祈使句天然就是指令。
+
+---
+
+## 五、设计维度一:信息放在哪里?
+> 框架位置:第二层 — 维度一
+
+在第三章的框架总览中,我们已经看到了三级分层架构的全貌。这一章展开讲它的细节。
+
+### 5.1 三级渐进式加载
+
+skill-creator 原文对三个层级的定义:
+
+> 1. **Metadata (name + description)** - Always in context (~100 words)
+> 2. **SKILL.md body** - When skill triggers (<5k words)
+> 3. **Bundled resources** - As needed by Codex (Unlimited because scripts can be executed without reading into context window)
+
+| 层级 | 内容 | 何时在上下文中 | token 成本 |
+|------|------|--------------|-----------|
+| **L1** | frontmatter(name + description) | **始终** | ~100 词 |
+| **L2** | SKILL.md body | 触发后加载 | <5k 词 |
+| **L3** | scripts/ references/ assets/ | 按需加载 | 无上限 |
+
+**这本质上是一个信息熵管理系统**:
+
+- **L1 是过滤器** — 从几十个已安装技能中筛选出当前需要的那一个。description 不精确 → 误触发或漏触发
+- **L2 是操作手册** — 触发后告诉 AI 该怎么做。太长 → 注意力被稀释。body 控制在 500 行以内
+- **L3 是工具箱** — 只在需要时打开。其中 scripts/ 最高效——**执行而不读入**,零 token 成本
+
+### 5.2 Frontmatter:触发机制的全部来源
+
+Frontmatter 只有两个必需字段:`name` 和 `description`。但 description 的写法至关重要:
+
+> This is the primary triggering mechanism for your skill, and helps Codex understand when to use the skill.
+
+skill-creator 自己的 description 是这样写的:
+
+```yaml
+description: Guide for creating effective skills. This skill should be used when
+  users want to create a new skill (or update an existing skill) that extends
+  Codex's capabilities with specialized knowledge, workflows, or tool integrations.
+```
+
+它不只说"做什么"(creating effective skills),还说"什么时候用"(when users want to create a new skill or update an existing skill)。
+
+**关键规则**:
+- 把所有"when to use"信息放在 description 里,**不要放在 body 里**。body 是触发后才加载的,那时候 Codex 已经决定用了,"什么时候用"的信息已经迟了
+- 不要在 frontmatter 中放 `name` 和 `description` 以外的字段(`license`、`allowed-tools`、`metadata` 除外)
+
+一个好的 description 示例(docx 技能):
+
+> "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use when Codex needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks"
+
+### 5.3 四种捆绑资源的本质区别
+
+理解这四种资源的区别,是理解整个 skill 系统的关键:
+
+#### Scripts(`scripts/`)
+
+可执行代码(Python/Bash 等),用于需要**确定性可靠性**或反复重写的任务。
+
+- **什么时候需要**:同样的代码每次都要重新写,或者需要确定性的可靠输出
+- **举例**:`scripts/rotate_pdf.py` 用于 PDF 旋转任务
+- **核心优势**:token 高效、确定性、可以执行而不读入上下文窗口
+- **注意**:脚本有时仍需要被 Codex 读取,用于修补或环境适配
+
+#### References(`references/`)
+
+文档和参考材料,在需要时加载到上下文中,辅助 Codex 的思考过程。
+
+- **什么时候需要**:Codex 在工作时需要参考的详细文档
+- **举例**:`references/finance.md`(财务 schema)、`references/api_docs.md`(API 规范)、`references/policies.md`(公司政策)
+- **用途**:数据库 schema、API 文档、领域知识、公司政策、详细工作流指南
+- **核心优势**:保持 SKILL.md 精炼,只在 Codex 判断需要时才加载
+- **最佳实践**:如果文件很大(>10k 词),在 SKILL.md 中包含 grep 搜索模式
+- **避免重复**:信息应该只存在于 SKILL.md **或** references 文件中,不能两边都有。详细信息优先放 references,SKILL.md 只保留核心流程指令和工作流指导
+
+#### Assets(`assets/`)
+
+不是用来加载到上下文中的文件,而是直接用在 Codex 产出物中的资源。
+
+- **什么时候需要**:技能需要在最终输出中使用的文件
+- **举例**:`assets/logo.png`(品牌素材)、`assets/slides.pptx`(PPT 模板)、`assets/frontend-template/`(HTML/React 样板)、`assets/font.ttf`(字体)
+- **用途**:模板、图片、图标、样板代码、字体、示例文档——这些会被复制或修改
+- **核心优势**:将输出资源与文档分离,Codex 可以使用它们而无需读入上下文
+
+#### Agents 元数据(`agents/openai.yaml`)(推荐)
+
+面向 UI 的元数据,不给 AI 读,给产品前端读:
+
+- 包含 `display_name`、`short_description`、`default_prompt` 等字段
+- 通过脚本 `generate_openai_yaml.py` 确定性生成,而不是手写
+- 更新 SKILL.md 后要检查 `agents/openai.yaml` 是否还匹配,过期了就重新生成
+- 详细字段定义见 `references/openai_yaml.md`
+
+### 5.4 渐进式披露的三种实战模式
+
+Skill-creator 给出了三种把内容拆分到 references 的具体模式:
+
+**Pattern 1:高层指南 + 参考文件**
+
+```markdown
+# PDF Processing
+
+## Quick start
+Extract text with pdfplumber:
+[code example]
+
+## Advanced features
+- **Form filling**: See [FORMS.md](FORMS.md) for complete guide
+- **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods
+- **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns
+```
+
+Codex 只在需要时才加载 FORMS.md、REFERENCE.md 或 EXAMPLES.md。
+
+**Pattern 2:按领域组织**
+
+多领域/多变体技能,按领域拆分避免加载无关内容:
+
+```
+bigquery-skill/
+├── SKILL.md (overview and navigation)
+└── reference/
+    ├── finance.md (revenue, billing metrics)
+    ├── sales.md (opportunities, pipeline)
+    ├── product.md (API usage, features)
+    └── marketing.md (campaigns, attribution)
+```
+
+用户问销售指标时,Codex 只读 `sales.md`。
+
+同样适用于多框架/多变体场景:
+
+```
+cloud-deploy/
+├── SKILL.md (workflow + provider selection)
+└── references/
+    ├── aws.md (AWS deployment patterns)
+    ├── gcp.md (GCP deployment patterns)
+    └── azure.md (Azure deployment patterns)
+```
+
+**Pattern 3:条件性细节**
+
+基础功能直接展示,高级功能按需链接:
+
+```markdown
+# DOCX Processing
+
+## Creating documents
+Use docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md).
+
+## Editing documents
+For simple edits, modify the XML directly.
+
+**For tracked changes**: See [REDLINING.md](REDLINING.md)
+**For OOXML details**: See [OOXML.md](OOXML.md)
+```
+
+### 5.5 两条重要的避坑指南
+
+1. **避免深层嵌套引用** — 所有 reference 文件应该从 SKILL.md 直接链接,不要 A → B → C 式嵌套
+2. **长文件加目录** — 超过 100 行的 reference 文件要在顶部加 TOC,方便 Codex 预览全貌
+
+### 5.6 常见的层错位
+
+| 错误 | 后果 | 修正 |
+|------|------|------|
+| 触发条件放在 body 里 | body 是触发后才加载的,晚了 | 放 frontmatter description |
+| "When to Use This Skill" 写在 body | 同上,Codex 已经决定用了才看到 | 移到 description |
+| 参考细节塞进 SKILL.md | body 膨胀,信息密度下降 | 拆到 references/,body 只放引用链接 |
+| 确定性操作写成文字指令 | AI 每次重新理解,可能出错 | 封装成 scripts/,执行不读入 |
+| references 互相引用 | AI 需要多跳获取信息 | 所有 references 从 SKILL.md 直接链接 |
+| SKILL.md 和 references 内容重复 | 浪费 token,更新时可能不一致 | 信息只在一处存在 |
+
+---
+
+## 六、设计维度二:给 AI 多大自由度?
+> 框架位置:第二层 — 维度二
+
+知道了信息该放在哪里、该怎么约束,下一个问题是:**AI 做什么,脚本做什么?**
+
+AI 非常擅长理解语义、生成文本、做创造性工作。但它不擅长精确格式控制、长度约束、命名规范——这些"脆弱操作"。
+
+### 6.1 三个自由度档位
+
+Skill-creator 用一个**自由度光谱**来处理这种不均匀性(见第三章框架图):
+
+> Think of Codex as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom).
+
+**高自由度**(文字指令):多种方法都可行时,决策依赖上下文,用启发式引导。
+
+**中自由度**(伪代码/带参数的脚本):有最佳实践但允许变通,配置影响行为。
+
+**低自由度**(具体脚本,少量参数):操作脆弱容易出错,一致性至关重要,必须遵循特定序列。
+
+核心逻辑:
+
+```
+任务越脆弱(容易出错) → 自由度越低 → 用脚本锁死
+任务越灵活(多种方案都对) → 自由度越高 → 用文字引导
+```
+
+### 6.2 skill-creator 自身的自由度分配
+
+| 任务 | 自由度 | 实现方式 |
+|------|--------|---------|
+| 理解用户需求并提问 | 高 | SKILL.md 文字指导 |
+| 规划技能内容结构 | 中 | 模板 + 选择题式模式推荐 |
+| 初始化目录结构 | **低** | `init_skill.py` 脚本 |
+| 生成 openai.yaml | **低** | `generate_openai_yaml.py` 脚本 |
+| 编写 SKILL.md 内容 | 高 | 原则指导 + 写作建议 |
+| 校验最终结果 | **低** | `quick_validate.py` 脚本 |
+
+### 6.3 两个方向的错误
+
+**错误 1:给脆弱任务太多自由度**
+
+```markdown
+# 错误
+请生成一个 openai.yaml 文件,包含 display_name 和 short_description。
+
+# 后果:short_description 可能超过 64 字符限制,大小写可能不一致
+```
+
+Skill-creator 的做法:用 `generate_openai_yaml.py` 脚本锁死格式。AI 只提供参数值,脚本保证输出合规。
+
+**错误 2:给创造性任务太多约束**
+
+```markdown
+# 错误
+第一段必须以"昨天"开头,第二段必须包含"本质上",最后一段以"慢慢来"结尾。
+
+# 后果:生成的文本像填词游戏
+```
+
+Skill-creator 的做法:给结构比例(场景层 ≤30%,原理层 30-40%),但不锁定具体用词。
+
+### 6.4 判断标准
+
+两个问题:
+1. **做错了后果多严重?** — 越严重 → 越低自由度
+2. **有多少种"正确"的做法?** — 越多 → 越高自由度
+
+### 6.5 低自由度的实现:skill-creator 的三个脚本
+
+理解了自由度光谱,就能理解 skill-creator 为什么有三个脚本——它们就是"低自由度"的具体实现(脚本间的交互关系见第三章框架图)。
+
+**`init_skill.py`(输入保障,398 行)**
+
+初始化新技能目录的脚手架工具,类似 `create-react-app` 之于 React 项目:
+
+```bash
+scripts/init_skill.py <skill-name> --path <output-directory> \
+  [--resources scripts,references,assets] [--examples] \
+  [--interface key=value]
+```
+
+核心功能:
+- 创建技能目录
+- 生成带 TODO 占位符的 SKILL.md 模板(TODO 是给 Codex 看的"填空题")
+- 调用 `generate_openai_yaml.py` 生成 `agents/openai.yaml`(通过 `--interface key=value` 传入 AI 生成的 display_name、short_description、default_prompt)
+- 可选创建 `scripts/`、`references/`、`assets/` 子目录
+- 可选添加示例文件(`--examples`)
+- 内置 `normalize_skill_name()` 自动把任意用户输入标准化为 hyphen-case
+
+使用示例:
+```bash
+scripts/init_skill.py my-skill --path skills/public
+scripts/init_skill.py my-skill --path skills/public --resources scripts,references
+scripts/init_skill.py my-skill --path skills/public --resources scripts --examples
+```
+
+**`generate_openai_yaml.py`(格式保障,226 行)**
+
+专门负责生成和更新 `agents/openai.yaml`:
+
+- 从 SKILL.md 的 frontmatter 读取技能名
+- 自动将 hyphen-case 转为 Title Case(`my-cool-skill` → `My Cool Skill`)
+- 内置缩写词典(GH、MCP、API 等保持大写)和品牌词典(openai → OpenAI)
+- 自动生成 25-64 字符的 `short_description`
+- 支持 `--interface key=value` 覆盖任意字段
+
+```bash
+scripts/generate_openai_yaml.py <path/to/skill-folder> --interface key=value
+```
+
+**`quick_validate.py`(输出保障,102 行)**
+
+技能创建后的"质检员":
+
+```bash
+scripts/quick_validate.py <path/to/skill-folder>
+```
+
+校验内容:
+- SKILL.md 是否存在
+- YAML frontmatter 格式是否合法
+- `name`:是否为 hyphen-case,≤ 64 字符,无连续/首尾连字符
+- `description`:是否存在,无尖括号,≤ 1024 字符
+- 只允许 `name`、`description`、`license`、`allowed-tools`、`metadata` 这 5 个 frontmatter 键
+
+### 6.6 质量保障链
+
+三个脚本形成了一条**确定性保障链**,夹住中间的创造性步骤:
+
+```
+init_skill.py(输入保障)
+  命名标准化 + 目录结构创建 + 模板生成
+  → 确保起点正确
+       ↓
+  AI 创造性编写(高自由度)
+  → SKILL.md 内容、references、自定义 scripts
+       ↓
+quick_validate.py(输出保障)
+  frontmatter 格式 + 命名规范 + 长度约束校验
+  → 确保终点合规
+```
+
+关键洞察:脚本是"执行而不读入"的——**零 token 成本**。你可以把任意复杂的确定性逻辑封装进脚本,而不用担心它占用上下文。这就是为什么 skill-creator 把命名转换(缩写词典、品牌词典)、长度约束(25-64 字符)、格式校验这些细碎但脆弱的操作全部交给了脚本。
+
+### 6.7 什么该封装成脚本?
+
+```
+每次执行结果必须一样      → 脚本
+涉及精确格式/长度约束     → 脚本
+涉及命名规范转换          → 脚本
+需要校验规则匹配          → 脚本
+同样的代码每次都要重新写   → 脚本
+
+需要理解上下文            → 文字指令
+有多种合理做法            → 文字指令
+需要创造性判断            → 文字指令
+```
+
+脚本有时仍需要被 Codex 读取(用于修补或环境适配),但大多数时候它们是"执行而不读入"的。
+
+---
+
+## 七、落地:六步创建流程
+> 框架位置:第三层
+
+有了前面的原则和架构,skill-creator 最后给出了一个六步创建流程,把设计思想变成可执行的操作步骤(见第三章框架图)。
+
+### 7.0 命名规范
+
+在开始之前,先确定命名:
+
+- 只用小写字母、数字和连字符;把用户提供的名称标准化为 hyphen-case(如 "Plan Mode" → `plan-mode`)
+- 名称 ≤ 64 字符
+- 优先用简短的、动词开头的短语来描述动作
+- 需要时用工具名做命名空间(如 `gh-address-comments`、`linear-address-issue`)
+- 技能文件夹名与技能名完全一致
+
+### 7.1 Step 1:理解技能——用具体例子建立共识
+
+> Skip this step only when the skill's usage patterns are already clearly understood.
+
+要创建一个有效的 skill,必须先清楚理解**具体的使用例子**。这些理解可以来自用户提供的例子,也可以来自生成的、经用户验证的例子。
+
+以构建 image-editor 技能为例,可以问用户:
+
+- "image-editor 技能应该支持什么功能?编辑、旋转,还有其他吗?"
+- "能给一些使用这个技能的例子吗?"
+- "我能想到用户会说'去掉这张照片的红眼'或'旋转这张图片'。还有其他使用方式吗?"
+- "用户会说什么话来触发这个技能?"
+
+**注意**:不要一次问太多问题。先问最重要的,然后根据需要跟进。
+
+**完成标志**:对技能应该支持的功能有了清晰的认识。
+
+### 7.2 Step 2:规划可复用的技能内容
+
+对每个具体例子做两个分析:
+1. 如果从零开始做这件事,需要什么?
+2. 其中哪些会被反复使用?
+
+反复使用的东西 → 封装成 scripts/references/assets。
+
+skill-creator 给了三个典型分析案例:
+
+**案例 1:`pdf-editor` 技能**(用户问"帮我旋转这个 PDF")
+- 旋转 PDF 每次都要重写同样的代码
+- → 封装为 `scripts/rotate_pdf.py`
+
+**案例 2:`frontend-webapp-builder` 技能**(用户问"帮我做一个 todo app"或"做一个步数追踪仪表盘")
+- 写前端 webapp 每次都需要同样的 HTML/React 样板代码
+- → 封装为 `assets/hello-world/` 模板目录
+
+**案例 3:`big-query` 技能**(用户问"今天有多少用户登录了?")
+- 查询 BigQuery 每次都要重新发现表的 schema 和关系
+- → 封装为 `references/schema.md`
+
+**完成标志**:列出了所有要包含的可复用资源清单(scripts、references、assets)。
+
+### 7.3 Step 3:初始化技能
+
+> When creating a new skill from scratch, always run the `init_skill.py` script.
+
+这里用的是"always"——不是"建议",是"总是"。原因:
+- 脚本生成的目录结构保证符合规范
+- 模板中的 TODO 提醒确保不遗漏必需字段
+- `agents/openai.yaml` 的格式约束(字段长度、引号规则)靠手写容易出错
+
+这是**低自由度原则的直接应用**:初始化是一个脆弱操作,用脚本消除出错可能。
+
+初始化后:
+- 定制 SKILL.md 并根据需要添加资源
+- 如果用了 `--examples`,替换或删除占位符文件
+
+### 7.4 Step 4:编辑技能
+
+这是最核心的步骤,分两阶段:
+
+#### 阶段一:先实现可复用资源
+
+从 Step 2 规划的资源开始:实现 `scripts/`、`references/`、`assets/` 文件。
+
+注意:
+- 这一步可能需要用户输入(比如 `brand-guidelines` 技能需要用户提供品牌素材)
+- 新增的脚本**必须通过实际运行来测试**,确保无 bug 且输出符合预期
+- 如果有很多类似的脚本,只需测试代表性样本来建立信心
+- 如果用了 `--examples`,删除不需要的占位符文件。只创建真正需要的资源目录
+
+#### 阶段二:更新 SKILL.md
+
+**Frontmatter 写法**:
+
+```yaml
+---
+name: skill-name
+description: >-
+  描述技能做什么 + 具体什么时候用。
+  把所有 "when to use" 信息放这里,不要放在 body 里。
+---
+```
+
+**Body 写法**:
+
+写给另一个 Codex 实例的操作指令。包含对 Codex 有帮助但不显而易见的信息:程序性知识、领域细节、可复用资源的使用方式。
+
+统一使用**祈使语气/不定式**。
+
+### 7.5 Step 5:校验技能
+
+```bash
+scripts/quick_validate.py <path/to/skill-folder>
+```
+
+校验 YAML frontmatter 格式、必需字段、命名规则。不通过就修复后重新运行。
+
+### 7.6 Step 6:迭代
+
+> After testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed.
+
+迭代工作流:
+1. 在真实任务上使用技能
+2. 发现吃力或低效的地方
+3. 找出 SKILL.md 或捆绑资源该如何更新
+4. 实施变更并重新测试
+
+好的 skill 不是一次写成的。skill-creator 创建的 laotou-thought-style 技能,在第一次生成后就迭代了 `openai.yaml` 的 `short_description` 和 `default_prompt`——从泛泛的描述变为更精确的操作指令。
+
+---
+
+## 八、总结
+
+回到最初的问题:怎么写出好的 skill?
+
+回顾整个框架:
+
+```
+根本约束:简洁(第四章)
+ ├── 信息放在哪里? → 三级分层,按需加载(第五章)
+ ├── 给 AI 多大自由度? → 脆弱操作脚本锁死,创造性工作文字引导(第六章)
+ └── 怎么落地? → 六步流程:理解→规划→初始化→编辑→校验→迭代(第七章)
+```
+
+**Skill是给 AI 写指令,而不是给人。用最少的 token,在正确的层级,给 AI 最精准的约束,让它在边界内自由发挥。**

+ 1341 - 0
Extra-Chapter/Extra09-Agent应用开发实践踩坑与经验分享.md

@@ -0,0 +1,1341 @@
+# Agent应用开发实践踩坑与经验分享
+
+学完 Hello-Agents 教程之后,最后一个任务是毕业设计。用所学的知识自己手搓一个Agent应用,刚好那段时间 Code Agent 特别火,Cursor、Claude Code、Codex... 各家都在推自己的产品。心想既然要练手,不如复刻一个 Code Agent,自己手搓一遍,才能真正理解这些产品为什么好用,以及它们到底在工程上做对了什么。
+
+于是就有了这个项目。
+基于Hello-Agents框架的Code Agent代码仓库:https://github.com/datawhalechina/hello-agents/tree/main/Co-creation-projects/YYHDBL-HelloCodeAgentCli
+
+重构后MyCodeAgent代码仓库:https://github.com/YYHDBL/MyCodeAgent.git
+
+这篇文章不是教程,是我在做这个 Code Agent 项目过程中踩过的坑、走过的弯路、以及最后怎么解决的一些记录。  
+
+---
+
+## 目录
+
+- [第一章:看了太多最佳实践,反而踩进第一个大坑](#第一章看了太多最佳实践反而踩进第一个大坑)
+- [第二章:一次管道命令事故——我第一次看见"不可诊断"有多致命](#第二章一次管道命令事故我第一次看见不可诊断有多致命)
+- [第三章:工具设计的 Goldilocks 区](#第三章工具设计的-goldilocks-区)
+- [第四章:提示词不是魔法咒语,而是 Agent 的控制面](#第四章提示词不是魔法咒语而是-agent-的控制面)
+- [第五章:上下文不是内存容量问题,而是注意力调度问题](#第五章上下文不是内存容量问题而是注意力调度问题)
+- [第六章:可观测性把黑盒变玻璃盒](#第六章可观测性把黑盒变玻璃盒)
+- [第七章:从一个项目抽出来的通用方法论](#第七章从一个项目抽出来的通用方法论)
+
+
+---
+
+# 第一章:看了太多最佳实践,反而踩进第一个大坑
+
+刚动手写代码时,我查阅了大量业界的 Agent 设计实践。比如 Manus 团队分享的《上下文工程经验教训》,还有 Anthropic 官方的《Building agents with the Claude Agent SDK》。看着这些顶流大厂毫无保留地分享"最佳实践",我心想:反正现在有 Claude Code,让 AI 帮我把这些高级概念全实现一遍不就行了?
+
+于是,我不假思索地堆砌了各种看似优雅的设计:多层记忆(Memory System)、复杂的上下文工程、多智能体系统(Multi-Agent)……不得不说,Claude Code 确实牛逼,很快就帮我生成了一大堆逻辑复杂的代码。
+
+## 天崩开局
+
+但当我满怀期待地跑起第一版测试时,现实狠狠打了我一巴掌:整个系统烂透了。
+
+面对一个极其简单的修改需求,Agent 像发疯一样调用了七八种工具,进行了好几轮的"左右脑互搏"。最终,我只收获了一段根本跑不通的残缺代码,以及一张严重超支的 Token 欠费账单。
+
+看着满屏的报错,我才意识到:Agent 开发和传统软件开发很不一样。
+
+以前我们做传统后端开发,习惯先画好架构图,再写代码。图纸够优雅,系统就稳固。这是程序员的本能。
+
+但 Agent 开发不一样。你是在跟一个大模型打交道,它本身就是概率性的——同样的输入,每次可能给你完全不同的输出。
+
+我在这个不确定的地基上,强行叠加了一套自己都没验证过的复杂架构。多智能体、Plan-and-Execute……这些设计彼此交叉,让不确定性成倍放大。
+
+结果是:复杂架构没能兜住底,反而因为状态流转太多、工具交叉太复杂,让模型错得更离谱。错误在各组件之间来回传,我连排查都无从下手。
+
+那些大厂的"最佳实践"当然是好东西,但我忽略了一点:那些复杂架构是他们踩了无数坑、耗费了海量 token 之后演进出来的结果,不是新手上路的起点。
+
+## 推倒重来
+
+看着这堆连简单读取文件都会陷入死循环的代码,我做了一个违背祖宗的决定——删库,推倒重来。
+
+奉行"Less is more"的原则,我直接复用了 Hello-Agent 最基础的主干,把最短的链路先跑通。核心组件被精简到只剩这几块:
+
+| 组件 | 核心职责 |
+|------|----------|
+| ReActAgent | 驱动 Thought → Action → Observation 的基础认知循环 |
+| ToolRegistry | 负责工具的注册与调用分发 |
+| ContextBuilder | 拼接系统规则、历史记录与环境证据 |
+| TerminalTool | 在目标代码仓库内执行实际命令 |
+| Message | 统一的会话消息数据结构 |
+
+代码层面,我没有搞任何花哨的设计模式,直接在 code_agent.py 里粗暴地把它们攒了起来:
+
+```python
+self.terminal_tool = TerminalTool(
+    workspace=str(self.paths.repo_root),
+    timeout=60,
+    confirm_dangerous=True,
+    default_shell_mode=True,  
+)
+self.registry = ToolRegistry()
+self.registry.register_tool(self.terminal_tool)
+```
+
+看着这个简陋的 V0 版本成功在终端里跑通了第一个极简任务,我长舒了一口气。
+
+![image-20260225143343219](images/Extra09-figures/1.png)
+
+只要可以完成最基础的基础对话和工具调用,我们就能在真实任务环境中验证和迭代优化。
+
+先跑起来,比一步更重要。
+
+
+---
+
+# 第二章:一次管道命令事故——我第一次看见"不可诊断"有多致命
+
+V0 跑起来之后,我开始放松对工具的约束。既然架构已经精简了,让模型自由组合命令应该没问题吧?
+
+于是我给 Terminal Tool 开了绿灯:`default_shell_mode=True`。这意味着模型不只能跑单条命令,还能写管道、重定向、子命令——就像人一样。
+
+## 事故经过
+
+那天我提了个简单需求:"帮我搜一下 `process_data` 函数的定义"。
+
+模型很快给出了一条看起来挺专业的命令:
+
+```bash
+rg -n "def process_data" src/ | grep -v test | sed -n '1,50p'
+```
+
+老工程师常用的组合拳:先搜函数定义,过滤掉测试文件,再取前 50 行。没什么问题。
+
+但执行结果是空的。Agent 看到这个空结果,愣了一下,然后开始补救:
+
+**第一轮重试:**
+```
+Thought: 看起来没找到,可能是搜索范围太窄了。让我扩大搜索范围。
+Action: rg -n "process_data" | head -100
+```
+
+还是空。
+
+**第二轮重试:**
+```
+Thought: 可能函数名记错了,试试模糊匹配。
+Action: rg -n "process.*data" src/ --type py | grep "def "
+```
+
+还是空。
+
+**第三轮重试:**
+```
+Thought: 看来这个函数可能不存在,或者我搜错了目录。让我先列出目录结构。
+Action: find src/ -name "*.py" | xargs grep -l "process_data"
+```
+
+结果依然是空。三轮尝试后,它放弃了,告诉我:"我在仓库里没有找到 `process_data` 函数的定义,可能函数名有误,或者它不在 Python 文件里。"
+
+但我手动去仓库里看了,那个函数明明就在 `src/utils/helpers.py` 第 42 行。
+
+## 排查过程
+
+我复制那条命令到终端自己跑,发现 `rg` 报错了:
+
+```bash
+$ rg -n "def process_data" src/ | grep -v test | sed -n '1,50p'
+Error: path 'src/' does not exist
+```
+
+原来我启动 Agent 时的工作目录不是项目根目录,而是项目下的一个子目录。`src/` 相对当前目录不存在,rg 直接报错退出。
+
+但在 Agent 那边,错误信息被管道吞掉了。因为命令用了 `|`,rg 的错误输出没有传到 stdout,而是被管道导向了下一个命令的输入。grep 收到的是空输入,自然输出空;sed 也是空。
+
+**错误在链路中被压扁了。** Agent 看到的只是一个空字符串,它根本不知道上游失败了。
+
+最坑的是,模型基于这个错误信息做出了完全错误的判断。它以为"确实没找到",于是开始各种补救:换搜索词、换目录、甚至怀疑我是不是记错了函数名。这些动作全都是基于一个错误的判断,白白消耗了大量 token。
+
+![image-20260225143439420](images/Extra09-figures/2.png)
+
+## 当时的错误修复方向
+
+我第一反应是:Bash 工具太危险了,得加限制。
+
+于是我写了一大堆安全检查代码:
+
+```python
+SHELL_META_TOKENS = ["|", "||", "&&", ";", ">", ">>", "<", "$(", "`"]
+DANGEROUS_BASE_COMMANDS = {"rm", "chmod", "mv", "dd"}
+
+def validate_command(cmd):
+    # 检查是否包含管道或重定向
+    for token in SHELL_META_TOKENS:
+        if token in cmd:
+            return False, f"包含非法字符: {token}"
+    
+    # 检查基础命令是否在白名单
+    base_cmd = cmd.split()[0]
+    if base_cmd not in ALLOWED_COMMANDS:
+        return False, f"命令 {base_cmd} 不在白名单"
+    
+    # 检查危险命令
+    if base_cmd in DANGEROUS_BASE_COMMANDS:
+        return False, "危险命令,禁止执行"
+    
+    return True, "OK"
+```
+
+但很快我发现,shell 太灵活了。你禁了 `|`,它可以用 `$(...)` 子命令替换;你禁了 `>`,它可以用 `tee`;你禁了 `rm`,它可以用 `> file` 来清空文件。
+
+补丁越打越多,代码越写越长,但那个根本问题——"到底是哪一步失败了"——依然存在。
+
+即使我封死了所有管道和重定向,只允许最简单的单条命令,问题还在:
+
+```bash
+rg "pattern" src/
+```
+
+如果返回空,我还是不知道是"仓库里真的没有",还是"rg 因为路径错误没执行"。模型依然无法针对性地纠错。
+
+## 根因定位
+
+后来我才想明白,这件事的根因不是"命令太危险",而是**不可诊断**。
+
+具体来说有三个问题:
+
+**第一,多步骤被塞进一个 Action。** 管道把好几步逻辑打包在一起,中间状态全丢了。Agent 只能看到最终结果,看不到执行过程。
+
+**第二,观察信号只有一个终态。** 成功、失败、空结果,全都混在一起。模型分不清楚"真的没找到"和"查找过程中出错了"。
+
+**第三,模型无法针对性纠错。** 它不知道 `rg`、`grep`、`sed` 谁出了问题,下一步只能瞎猜。重试不是基于"修正错误",而是基于"赌运气"。
+
+给模型更高自由度,不是在提升能力上限,而是在放大不确定性。它确实能写出更"聪明"的命令,但一旦出错,连你自己都排查不了它在哪一步"聪明反被聪明误"了。
+
+## 现在的做法
+
+后来我直接把 Bash 降级了——不是删掉,而是明确它的定位:只处理那些原子工具覆盖不到的边角需求,不走主链路。
+
+高频操作全部拆成原子工具:
+
+| 工具 | 功能 | 返回格式 |
+|------|------|----------|
+| LS | 列目录 | `{status, data: {entries}, text}` |
+| Glob | 按名字找文件 | `{status, data: {paths}, text}` |
+| Grep | 按内容搜索 | `{status, data: {matches}, text}` |
+| Read | 带行号读取 | `{status, data: {content}, text}` |
+
+每个工具都有明确的状态码:
+- `success`:任务完成,结果在 data 里
+- `partial`:任务完成但内容被截断
+- `error`:任务失败,error 里有具体错误码
+
+比如 Glob 搜不到文件:
+```json
+{
+  "status": "success",
+  "data": {"paths": []},
+  "text": "No files matching '*.xyz' found"
+}
+```
+
+路径不存在:
+```json
+{
+  "status": "error",
+  "error": {"code": "NOT_FOUND", "message": "Path 'src/' does not exist"}
+}
+```
+
+模型能清晰区分"确实没有"和"出错了"。
+
+Bash 的硬约束也明确了:
+- 禁止读/搜/列:`ls`/`cat`/`head`/`grep`/`find`/`rg` 这些有专门工具
+- 禁止交互:vim、nano、top、ssh
+- 禁止网络(默认):curl/wget 被禁
+- 黑名单:rm -rf /、sudo/su、mkfs/fdisk
+
+这样做之后,调试变得简单很多。出了问题看日志就知道是哪一步:
+- Glob 返回了空数组 → 确实没这个文件
+- Glob 返回了 NOT_FOUND → 路径错了
+- Grep 返回了 timeout → 搜索范围太大
+
+模型也能根据具体的错误码决定下一步:路径错了就换路径,超时了就缩小范围,真的没找到就告诉用户。
+
+## 本章结论
+
+**可诊断性是可恢复性的前提。**
+
+如果不知道哪坏了,就修不好。如果不知道失败发生在哪一步,就无法针对性纠正。
+
+在 Agent 开发里,给模型自由组合命令的能力,听起来很美好,但实际上是在制造黑盒。看似高效的管道命令,把错误信息压扁成一个个无法区分的空结果,让模型在错误的道路上越跑越远。
+
+原子工具虽然步骤繁琐,但每一步都有明确的输入、输出、状态。出了问题,你能定位;模型错了,你能纠正。
+
+**可控性比一次性完成任务重要得多。**
+
+
+---
+
+# 第三章:工具设计的 Goldilocks 区——不是越自由越好,也不是越碎越好
+
+第三章之后,我开始把工具拆开。Terminal Tool 那种什么都管的万能模式确实有问题,拆成原子工具后,调试变得清晰多了。
+
+但我很快又踩了一个新坑:**拆得太碎了**。
+
+## 两个极端我都踩过
+
+### 极端 A:万能工具
+
+第一个极端你已经见过了。一个 Terminal Tool 什么都能做:管道、重定向、子命令、环境变量——完全放开。
+
+那时候我觉得,LLM 这么聪明,给它足够自由度,应该能像工程师一样操作。`rg | grep | sed` 这种组合命令效率很高。
+
+结果你也知道了:错误被管道吞掉,模型瞎猜重试,token 哗哗流,问题还没解决。
+
+### 极端 B:过度原子化
+
+意识到万能工具有问题后,我走向了另一个极端:把每个功能点都拆成独立工具,追求极致的原子化。
+
+那时候我的工具列表长这样:
+- `ListDir`:列出目录内容
+- `ListDirRecursive`:递归列出目录
+- `FindByName`:按文件名查找
+- `FindByPattern`:按通配符查找
+- `SearchExact`:精确匹配搜索
+- `SearchRegex`:正则匹配搜索
+- `SearchFuzzy`:模糊匹配搜索
+- `ReadLines`:读取指定行范围
+- `ReadOffset`:读取指定字节偏移
+- `ReadFull`:读取完整文件
+- ...
+
+问题很快就来了。
+
+**第一,模型开始"选工具困难"。**
+
+都是找文件,`FindByName`、`FindByPattern`、`Glob`,用哪个?模型经常在第一步就卡住,它要花好几轮才能确定"哦,原来应该用 Glob"。
+
+有一次我让它"找一下所有测试文件",它先调了 `ListDirRecursive` 列出所有文件,然后想调 `SearchRegex` 来过滤,但发现 `SearchRegex` 是搜内容不是搜文件名,于是又调回 `ListDirRecursive` 拿更多上下文,最后才选对 `Glob`。
+
+本来一步搞定的事,用了四步。
+
+**第二,Schema 噪声淹没上下文。**
+
+每个工具都有参数描述、类型定义、约束条件。十几个工具的 schema 加起来,几千 token 就出去了。
+
+模型还没开始解决任务,就先消耗大量注意力在"读说明书"上。更糟糕的是,长 schema 容易让模型"选择性失明"——它可能只注意到部分工具,或者把参数搞混。
+
+**第三,维护成本爆炸。**
+
+每个工具都要单独写测试、单独调优、单独处理边界情况。`FindByName` 和 `FindByPattern` 有 80% 的逻辑是重复的,但因为是两个独立工具,我得维护两份代码。
+
+这时候我才意识到,**工具系统不是乐高颗粒越细越好**。过度封装和过度拆分,本质上都会把系统推向不稳定,只是一个坏在执行期(万能工具),一个坏在决策期(过度原子化)。
+
+## 转折点:找那个"刚刚好"的度
+
+我后来给自己定了一个判断框架:**频率 × 确定性**。
+
+- **高频、强确定动作**:必须原子化,一步完成,不可再分
+- **中频、带副作用动作**:必须受控,关键操作加保险
+- **低频、弱确定动作**:保留弹性,但放到兜底层,明确禁止什么而非允许什么
+
+按这个框架,我重新设计了工具体系,形成三层结构:
+
+| 层级 | 代表工具 | 设计目标 | 典型约束 |
+|------|---------|---------|---------|
+| 高频原子层 | LS / Glob / Grep / Read | 一步一证据,便于纠错 | 输入输出强约束 |
+| 中频受控层 | Write / Edit / MultiEdit | 改动可验证、可回滚 | 读后写 + 乐观锁 |
+| 低频兜底层 | Bash | 处理非常规需求 | 明确禁区,不走主链 |
+
+![image-20260225143700868](images/Extra09-figures/3.png)
+
+这套分层不是"架构美学",是被真实故障逼出来的。它最大的价值是降低模型决策负担,让高频路径更短、更清晰。
+
+## 高频原子层:必须稳定
+
+这层工具是 Agent 的"主力武器",使用频率最高,必须极致稳定。
+
+### Glob:找文件,一个工具就够了
+
+最开始我想把"按名找文件"拆成多个工具:
+- `FindByName`:精确匹配文件名
+- `FindByPattern`:通配符匹配
+- `FindByRegex`:正则匹配
+- `FindRecursive`:递归查找
+
+后来我发现这就是过度原子化。模型会纠结:"我是该用精确匹配还是通配符?要不要递归?"
+
+最后合并成一个 `Glob`,只做一件事:给定模式,返回候选路径。
+
+```python
+# Glob 的参数
+{
+  "pattern": "**/*.py",  # 通配符模式,** 表示递归
+  "path": "src/"         # 起始路径,默认为当前目录
+}
+```
+
+内部实现可以复杂(支持 `**` 递归、自动处理大小写、结果排序),但对模型暴露的接口必须简单。模型不需要知道"递归还是不递归",它只需要说"找所有 py 文件"。
+
+### Grep:复杂度留在实现层
+
+Grep 是另一个例子。内部我做了很多优化:
+- 优先用 `rg`(ripgrep),速度快
+- `rg` 不可用时(比如编码问题、权限问题)自动回退到 Python 实现
+- 结果按文件修改时间排序,最近修改的排前面
+
+但对模型来说,它看到的就是:
+
+```python
+# Grep 的参数
+{
+  "pattern": "def process_data",  # 搜索模式
+  "path": "src/",                  # 搜索路径
+  "file_pattern": "*.py"          # 可选:只搜特定类型文件
+}
+```
+
+返回格式固定:
+```json
+{
+  "status": "success",
+  "data": {
+    "matches": [
+      {"file": "src/utils.py", "line": 42, "text": "def process_data(...)"},
+      {"file": "src/helpers.py", "line": 88, "text": "def process_data(...)"}
+    ]
+  }
+}
+```
+
+模型看到的是一个稳定入口。内部实现可以复杂(比如自动回退),但对外接口要简单。
+
+## 中频受控层:能改,但必须"读过才能改"
+
+这层工具涉及文件修改,是"高危操作",必须有严格的约束机制。
+
+### Read → Edit/Write 的强制顺序
+
+我设计了一个硬性规则:**不 Read 就不能改**。
+
+```python
+# 第一次 Read
+result = Read({"path": "core/llm.py"})
+# 返回包含 file_mtime_ms 和 file_size_bytes
+
+# 后续 Edit 自动注入乐观锁参数
+Edit({
+  "path": "core/llm.py",
+  "old_string": "...",
+  "new_string": "...",
+  "file_mtime_ms": 1733920000123,  # 自动注入
+  "file_size_bytes": 4217          # 自动注入
+})
+```
+
+`ToolRegistry` 会自动维护一个读缓存。如果某个文件没有被 Read 过,Edit/Write 会直接返回错误:`"File not read. You must read before editing."`
+
+这防止了模型"凭记忆"去改文件——它必须先把文件内容拿到上下文中,确认过,才能改。
+
+### 乐观锁:防止并发修改
+
+即使 Read 过了,文件也可能在 Read 之后被外部程序(比如 IDE 的自动保存)修改。
+
+Edit/Write 会对比 `file_mtime_ms` 和 `file_size_bytes`,如果不匹配,返回 `CONFLICT` 错误:
+
+```json
+{
+  "status": "error",
+  "error": {
+    "code": "CONFLICT",
+    "message": "File changed since last read."
+  }
+}
+```
+
+这时候模型必须重新 Read,获取最新内容,再尝试修改。
+
+### MultiEdit:原子性多点修改
+
+有时候需要在同一个文件里改多个地方。如果拆成多个 Edit,中间可能出错,导致文件处于"半改"状态。
+
+`MultiEdit` 支持一次性提交多个修改,要么全成功,要么全失败:
+
+```python
+MultiEdit({
+  "path": "core/llm.py",
+  "edits": [
+    {"old_string": "...", "new_string": "..."},
+    {"old_string": "...", "new_string": "..."}
+  ]
+})
+```
+
+这保证了文件修改的原子性。
+
+## 低频兜底层:Bash 不是不能用,但绝不能当默认入口
+
+Bash 我没删,因为总有原子工具覆盖不到的低频场景。比如:
+- 跑测试命令:`pytest tests/`
+- 安装依赖:`pip install -r requirements.txt`
+- 检查 git 状态:`git status`
+
+但它的定位必须是"兜底",不是"默认"。
+
+### 明确禁区
+
+Bash 的约束列表很长,但核心就一条:**禁止做高频动作能做的事**。
+
+```python
+BASH_DISABLED_PATTERNS = [
+    # 禁止读/搜/列(这些有专门工具)
+    r'\bls\b', r'\bcat\b', r'\bhead\b', r'\btail\b',
+    r'\bgrep\b', r'\bfind\b', r'\brg\b',
+    # 禁止交互
+    r'\bvim?\b', r'\bnano\b', r'\btop\b', r'\bssh\b',
+    # 禁止网络(默认)
+    r'\bcurl\b', r'\bwget\b',
+    # 危险命令黑名单
+    r'\brm\s+-rf\b', r'\bsudo\b', r'\bsu\b',
+    r'\bmkfs\b', r'\bfdisk\b'
+]
+```
+
+如果模型试图用 Bash 做 `ls`,它会收到错误:`"Use LS tool instead of Bash for listing directories."`
+
+这强制模型走原子工具的主链路,不让它"抄近道"。
+
+### 为什么留着 Bash?
+
+有人可能会问:既然限制这么多,为什么不干脆删掉 Bash?
+
+因为**完美原子化是不现实的**。总有一些边缘需求:
+- 跑一个自定义的 Python 脚本
+- 检查系统环境变量
+- 执行项目特定的构建命令
+
+这些需求频率太低,不值得专门做成工具,但又确实需要。Bash 就是处理这些"长尾需求"的。
+
+关键是:**Bash 的存在不能影响主链路的稳定性**。它必须是"最后手段",不是"默认入口"。
+
+## 关键机制设计
+
+### 统一响应协议
+
+所有工具,无论高频中频低频,都返回统一格式的 JSON:
+
+![image-20260225143736353](images/Extra09-figures/4.png)
+
+以Glob工具的返回结果为例:
+
+```json
+{
+  "status": "partial",
+  "data": {
+    "paths": ["core/llm.py", "agents/codeAgent.py"],
+    "truncated": true
+  },
+  "text": "Found 2 files matching '**/*.py' (Scanned 12000 items, timed out)",
+  "stats": {"time_ms": 2010, "matched": 2},
+  "context": {"cwd": ".", "params_input": {"pattern": "**/*.py"}}
+}
+```
+
+这有几个好处:
+- 模型不需要学习不同工具的不同返回格式
+- 错误处理逻辑统一:看 `status`,如果是 `error` 看 `error.code`
+- 调试方便:所有工具的输出结构一致,Trace 记录也统一
+
+### ToolRegistry
+
+`ToolRegistry` 不只是工具注册表,它还干几件关键的事:
+
+**1. Schema 汇总**
+
+把每个工具的参数定义转成 JSON Schema,统一提供给模型:
+
+```python
+registry.get_openai_tools()  # 返回所有工具的 schema 列表
+```
+
+**2. 乐观锁自动注入**
+
+对于 Write/Edit/MultiEdit,自动注入 `file_mtime_ms` 和 `file_size_bytes`:
+
+```python
+def _inject_optimistic_lock_params(self, tool_name, parameters):
+    if tool_name in {"Write", "Edit", "MultiEdit"}:
+        path = parameters.get("path")
+        if path in self.read_cache:
+            parameters["file_mtime_ms"] = self.read_cache[path]["mtime"]
+            parameters["file_size_bytes"] = self.read_cache[path]["size"]
+```
+
+**3. 熔断机制**
+
+工具连续失败会被临时禁用,防止模型在坏工具上死循环:
+
+```python
+# 3 次失败熔断,300 秒后恢复
+if circuit_breaker.should_block(tool_name):
+    return {
+        "status": "error",
+        "error": {"code": "CIRCUIT_OPEN", "message": "Tool temporarily disabled"}
+    }
+```
+
+## 本章结论
+
+这一章最大的反直觉是:**工具既不是越多越好,也不是越原子越好。**
+
+万能工具的问题在于"自由度过高",不可诊断;过度原子化的问题在于"决策负担过重",效率低下。
+
+找到刚刚好的度的关键:
+
+1. **高频动作先原子化**:LS/Glob/Grep/Read 这些每天调用几十次的工具,必须把主路径做稳,不能出错。
+
+2. **中频动作加保险**:Write/Edit 这种涉及修改的工具,必须有读后写、乐观锁、原子性保证。
+
+3. **低频动作兜底线**:Bash 保留,但明确禁区,禁止它做高频动作能做的事,避免污染主链路。
+
+4. **协议统一**:所有工具说同一种语言(status/data/text/error),降低模型学习成本。
+
+5. **数量控制**:schema 总量控制在模型可承受范围内,不要让"读说明书"消耗太多注意力。
+
+第三章让我明白"自由会放大不确定性"。
+
+
+---
+
+# 第四章:提示词不是魔法咒语,而是 Agent 的控制面
+
+工具原子化之后,我以为问题主要在"工程实现"上,提示词嘛,差不多就行。结果我又踩了一个大坑:把提示词当成魔法咒语,以为只要找到"神级提示词",Agent 就能变聪明。
+
+## 我最早的三种错误
+
+### 错误 1:照抄"神级提示词"
+
+那时候我沉迷于搜集各种"顶级提示词"。GitHub 上那些标星几万、号称"让 GPT 突破限制"的 prompt,我一个个拿来试。
+
+印象最深的是一个"专家模式"提示词,大概意思是让模型扮演一个"拥有 20 年经验的资深工程师,思考严谨、代码优雅"。我把它塞进 System Prompt,满怀期待地测试。
+
+结果?Agent 确实变得更"自信"了——它开始频繁地给出它"认为"正确的答案,而不是基于仓库里的真实代码。搜不到的时候它就开始"合理推测",编出一些看起来很有道理但实际上并不存在的函数和类。
+
+后来我明白了:这种角色扮演式提示词,对 ChatGPT 聊聊天可能有用,但对 Code Agent 是毒药。它让模型更敢"猜",而不是更依赖证据。
+
+### 错误 2:凭感觉调优
+
+每次 Agent 表现不好,我的第一反应就是改提示词。加一条"不要猜测",感觉好点;再加一条"必须基于证据",好像又聪明了点。
+
+但这种"好像变聪明了"完全是我的主观感受。同样的提示词,换个任务可能就崩了。我甚至不知道是哪条改动起了作用,因为每次都是好几条一起改。
+
+有一次我加了一段很长的规则,告诉模型在遇到复杂任务时应该"先分解再执行"。结果它开始在每轮都输出"让我分解一下这个问题",然后列出一堆毫无意义的步骤,真正该干的事反而被淹没了。
+
+### 错误 3:先改提示词,再补观测
+
+这是最蠢的一个习惯。Agent 出错了,我不先去查 Trace 看它到底做了什么,而是直接改提示词试图"预防"下一次出错。
+
+比如有一段时间,Agent 经常在不合适的时候调用 Write 工具。我直接在提示词里加了一大段:"只有在确认用户需要修改时才调用 Write,否则应该先用 Read 查看"。
+
+结果模型开始疯狂调用 Read,每轮都读一堆文件,然后才决定是否要写。Token 消耗翻倍,但正确率并没有提高。
+
+后来看 Trace 才发现,真正的问题是上下文里缺少了"当前任务类型"的信息,模型根本不知道用户是想浏览还是修改。提示词里的"应该"再多,也补不上信息缺口。
+
+## 我后来改成的方式
+
+### 先记录,后优化
+
+现在我养成了一个习惯:Agent 出问题时,先不碰提示词,而是打开 Trace 看完整轨迹。
+
+看什么呢?
+- 模型在哪一步开始偏离预期?
+- 它做出错误决策时,上下文里有什么信息?缺了什么信息?
+- 工具返回的结果,模型理解对了吗?
+
+很多时候问题根本不在提示词。比如模型反复用错工具,可能是因为工具描述不够清晰;它开始胡言乱语,可能是因为上下文太长导致注意力分散。这时候改提示词是治标不治本。
+
+### 用 Trace 做对比实验
+
+当我确定需要改提示词时,我会用 Trace 做对比实验:
+
+1. 保持其他所有条件不变,只改提示词里的一个点
+2. 跑同样的测试用例,记录成功率、步数、token 消耗
+3. 对比新旧 Trace,看行为差异是否如预期
+
+有一次我想让模型在搜索时更"精准"一些,减少了提示词里关于搜索策略的描述,只保留了"使用精确的关键词"。结果对比 Trace 发现,模型确实少搜了很多无关文件,但漏搜率也上去了——它过于保守,错过了一些相关文件。
+
+这个反馈让我意识到,不能一味追求"少",而是要在"全"和"准"之间找平衡。
+
+### 单变量改动
+
+我以前喜欢一次性加好几条规则,觉得这样能"全面覆盖"。现在我知道这是在给自己挖坑——如果表现变好了,你不知道是哪条规则起作用;如果变差了,你也不知道该删哪条。
+
+现在我坚持单变量改动。哪怕觉得某个问题很明显,也要一条一条试,验证每一条的实际效果。
+
+## 提示词设计的三层结构
+
+经过这些踩坑,我总结了一个相对稳定的提示词结构,分成三层:
+
+### 第一层:边界层(Not to do)
+
+这层只写"禁止"和"底线",不解释为什么:
+
+- 禁止猜测:如果没有找到,直接说没找到,不要推测
+- 禁止越界:只能操作 `repo_root` 内的文件,禁止访问外部路径
+- 信息不足必须承认:如果上下文里没有足够信息,要求补充,不要瞎编
+
+这层规则很短,但每条都是红线。它们不告诉模型"应该怎么做",只告诉它"绝对不能做什么"。
+
+### 第二层:决策层(How to think)
+
+这层写决策逻辑,但尽量用过程而不是结果来描述:
+
+- 先证据后结论:任何改动建议必须有代码片段支撑
+- 优先可验证动作:能用工具确认的,不要靠推理
+- 一步一观测:每个 Action 之后必须有 Observation,不要跳步
+
+注意这里避免使用"聪明地"、"合理地"这种模糊的副词。模型不知道什么叫"聪明",但它知道"先调用 Grep 找到证据,再调用 Read 确认内容"这个流程。
+
+### 第三层:恢复层(When failed)
+
+这层写失败时的退化策略,告诉模型出错时该怎么办:
+
+- 工具返回空:检查参数是否正确,考虑换关键词重试
+- 遇到 CONFLICT(乐观锁冲突):必须重新 Read,获取最新状态后再 Edit
+- 连续 3 次失败:停止尝试,向用户报告具体错误
+
+这层很关键,因为 Agent 不可能永远成功。失败时能不能优雅降级,比成功时表现多好更重要。
+
+## 工程细节
+
+### System Prompt 保持稳定
+
+我把变化最少的内容放在 System Prompt:基础行为规则、工具描述、边界约束。这部分尽量不动,减少变量。
+
+动态的信息——当前任务描述、用户的特殊要求、Todo 列表——都放在 User Message 里。这样每次交互都可以灵活调整,而不用改 System Prompt。
+
+### 避免规则清单过长
+
+我曾经写过一个 3000 多 token 的 System Prompt,里面有 20 多条"注意事项"。结果模型开始"选择性失明"——它只能注意到其中一部分规则,哪条被注意到全凭运气。
+
+现在我坚持一个原则:System Prompt 不超过 1000 token。如果规则太多,说明我的约束设计有问题,应该从工具层或流程层解决,而不是靠提示词堆砌。
+
+### 具体例子优先于抽象描述
+
+以前我写"工具返回错误时要正确处理",模型根本不知道什么叫"正确处理"。
+
+现在我直接在提示词里给一个例子:
+
+```
+如果 Edit 返回 CONFLICT,你应该:
+1. 重新 Read 该文件
+2. 对比你的改动和文件当前内容
+3. 如果需要,调整 old_string 以匹配新内容
+4. 再次尝试 Edit
+```
+
+具体步骤比抽象要求有用得多。
+
+## 本章结论
+
+好提示词不是"更会说",而是"让系统在失败时也可控"。
+
+当你设计提示词时,不要问自己"这样写能让模型更聪明吗",而要问"当模型出错时,我能不能通过提示词里的约束快速定位原因"。
+
+提示词是 Agent 的控制面,不是魔法咒语。它的作用不是让模型突破能力上限,而是把模型的行为约束在一个可预测、可调试的范围内。
+
+
+---
+
+# 第五章:上下文不是内存容量问题,而是注意力调度问题
+
+提示词调顺之后,我以为主要的工程问题都解决了。直到我开始跑长任务——那些需要十几轮、甚至几十轮才能完成的复杂需求。
+
+然后我发现,Agent 开始"变笨"了。
+
+## 症状先行
+
+最直观的感受是:模型会忘记它刚刚确认过的事情。
+
+有一次我让 Agent 重构一个模块,开头几轮它还记得"不要改动公共 API"的约束。但到了第 10 轮左右,它开始提议修改那些本该保持稳定的接口。我提醒它,它似乎"愣了一下",然后道歉,回到正轨。
+
+类似的症状还有很多:
+
+**工具选择漂移**。前期它很明确:找文件用 Glob,搜内容用 Grep。但对话一长,它开始"创新"——用 Read 去搜关键词(当然找不到),或者用 Grep 去列目录(输出混乱)。
+
+**最终回答偷懒**。短任务里,模型的回答通常很具体,会引用代码片段。但长任务结束时,它往往只给一段笼统的描述:"我已经完成了重构,优化了代码结构,提高了可读性。"什么文件改了、怎么改的,一概不提。
+
+这些症状指向一个共同的问题:上下文太多了,模型不知道看哪里。
+
+## 我的第一反应是错的
+
+一开始,我以为这是"容量"问题——上下文窗口不够大,塞不下这么多信息。
+
+我尝试了几种粗暴的方案:
+
+**方案一:直接截断**。只保留最近 N 条消息,老的直接删掉。结果模型彻底失忆,连用户最初的需求都忘了。
+
+**方案二:精简提示词**。把 System Prompt 砍到最短,工具描述也压缩。结果模型开始用错工具,因为描述不够清晰。
+
+**方案三:减少工具输出**。让 Grep 只返回前 10 条结果,Read 只读前 50 行。结果关键信息被截掉了,模型基于不完整的信息做决策,错得更离谱。
+
+这些方案有个共同点:它们在"减少信息量",但没有解决"信息如何被组织"的问题。上下文工程的目标不是"让模型看见所有信息"——这不可能——而是"让模型在对的时机看见对的信息"。
+
+## 分层:让信息有优先级
+
+我重新设计了上下文的组织结构,分成三层,每层有不同的更新频率和稳定性:
+
+| 层级 | 内容 | 更新频率 | 作用 |
+|------|------|----------|------|
+| L1 系统静态层 | System Prompt + 工具描述 | 几乎不变 | 提供永恒的行为准则 |
+| L2 项目规则层 | CODE_LAW.md | 随项目演进 | 项目特定的规范约束 |
+| L3 动态会话层 | User/Assistant/Tool 消息 | 每轮更新 | 当前任务的状态流转 |
+
+拼接顺序固定:`L1 → L2 → L3 → 当前用户输入 → Todo Recap`
+
+**L1 是锚点**。这部分在会话期间完全不变,模型可以信赖它。我把最基础的行为规则放在这里:不要猜测、不要越界、先证据后结论。这些规则不会因为对话变长而被"稀释"。
+
+**L2 是项目上下文**。每个项目可以有自己的 CODE_LAW.md,定义代码规范、架构约定、特殊约束。这层比 L1 灵活,但比 L3 稳定。模型知道:如果 CODE_LAW 里说了"所有 API 变更必须兼容旧版本",那它比 L3 里的某条历史消息更权威。
+
+**L3 是易变的**。用户输入、模型输出、工具返回,都在这里。这层的信息会累积、会过时、会有噪声。关键是让模型知道:L3 里的信息是"当时的判断",可能需要根据新信息更新。
+
+![image-20260225144114027](images/Extra09-figures/5.png)
+
+分层的意义在于:模型在不同的决策场景,知道应该优先参考哪一层。当它不确定该不该做某件事时,它会先看 L1 的底线规则;当它需要了解项目特定的约定时,它会看 L2;当它需要回顾对话历史时,它才会去翻 L3。
+
+## 截断与回查:控制单次输入的规模
+
+工具输出是上下文膨胀的最大元凶。
+
+一次 Grep 可能返回几千行,一次 Read 可能读出整个文件。如果不处理,几轮之后上下文就被"证据垃圾"淹没。
+
+但我之前的粗暴截断有问题——它直接把信息丢掉了。更好的做法是:**截断显示,但保留回查路径**。
+
+我设计了一套统一截断规则:
+
+```
+TOOL_OUTPUT_MAX_LINES = 2000
+TOOL_OUTPUT_MAX_BYTES = 51200  # 50KB
+TOOL_OUTPUT_TRUNCATE_DIRECTION = "head_tail"  # 保留头尾
+TOOL_OUTPUT_HEAD_TAIL_LINES = 40
+```
+
+如果输出超限,工具会:
+1. 截取头尾各 40 行(或者按配置保留前 2000 行)
+2. 把完整输出落盘到 `tool-output/` 目录
+3. 返回一个包含截断提示的结构化响应
+
+```json
+{
+  "status": "partial",
+  "data": {
+    "truncated": true,
+    "preview": "(截断后的内容预览)"
+  },
+  "text": "⚠️ 输出过大已截断,完整 5234 行内容见 tool-output/tool_20260113_153045_Grep.json"
+}
+```
+
+模型看到 `status: partial`,就知道内容被截断了。如果它需要被截掉的部分,可以用 Read 工具读取落盘文件,或者用更精确的 Grep 在落盘文件里进一步筛选。
+
+这样做的好处:
+- **上下文保持精简** —— 只有当前需要的信息在 L3 里
+- **完整证据始终可查** —— 落盘文件不会丢
+- **模型有主动权** —— 它决定要不要去查完整内容,而不是被迫接受所有信息
+
+## 压缩与聚焦:管理长期历史的噪音
+
+即使做了截断,L3 还是会不断增长。几十轮之后,早期的对话历史就变得既占空间又没什么用了。
+
+但我不能直接删掉——早期的历史里有用户最初的需求、关键的决策、重要的发现。删掉就真丢了。
+
+我的解决方案是:**压缩归档 + 焦点分离**。
+
+### Summary:旧历史的档案
+
+当 L3 的 token 数超过阈值(默认是上下文窗口的 80%)时,触发压缩。压缩不是删除,而是把早期的历史消息提炼成一份 Summary。
+
+Summary 按固定模板生成:
+
+```
+## Archived Session Summary
+(Contains context from [Start Time] to [Cutoff Time])
+
+### Objectives & Status
+- Original Goal: [用户最初想做什么]
+
+### Technical Context (Static)
+- Stack: [语言, 框架, 版本]
+
+### Completed Milestones
+- [已完成1]
+- [已完成2]
+
+### Key Insights & Decisions
+- Decisions: [关键技术选型]
+- Learnings: [特殊配置或坑]
+
+### File System State
+- src/utils/auth.ts: Implemented login logic.
+```
+
+Summary 生成后,被替换到 L3 的最前面(作为一条 system message)。原来的详细历史被移除。
+
+关键是:**Summary 不再参与压缩**。它是压缩的终点,一旦生成就是只读的"记忆卡片"。这避免了"Summary 的 Summary"这种层层失真。
+
+### Todo Recap:当前焦点
+
+Summary 告诉模型"从哪来",但它不负责"现在在哪"。如果模型只看 Summary,它可能不知道"我当前正在做哪一步"。
+
+这就是 Todo Recap 的作用。每次交互时,把当前的 Todo 状态(如果有的话)压缩成一行,放在上下文的最后:
+
+```
+[2/5] In progress: 实现注册接口. Pending: 添加单元测试; 更新文档.
+```
+
+它像一张贴在桌角的便利贴,时刻提醒模型"你现在该干嘛"。
+
+## 额外教训:@file 不要直接注入正文
+
+早期我实现 `@file` 功能时,是直接把文件内容塞进上下文的:
+
+```
+User: @file:src/main.py 帮我分析一下这个文件
+[文件内容300行...]
+```
+
+结果发现,这 300 行代码占据了上下文的大量空间,但用户可能只是想问"这个文件是干嘛的"。模型被这些代码淹没,反而容易忽略用户的真实问题。
+
+现在我改成:只插入提醒,不直接注入内容。
+
+```
+<system-reminder>
+The user mentioned @core/llm.py, @agents/codeAgent.py.
+You MUST read these files with the Read tool before answering.
+</system-reminder>
+```
+
+上下文里只保留"提醒",具体文件内容由模型自己决定要不要读、读多少。这样把主动权交给模型,而不是强迫它接受所有信息。
+
+## 一个真实世界的警示
+
+讲到这里,我想分享一个最近的新闻。
+
+Meta 超级智能实验室的 AI 对齐总监 Summer Yue,给自己装了一个开源 AI 智能体 OpenClaw。她先用测试邮箱试了试,效果不错——整理邮件井井有条,颇有一种"数字秘书"的感觉。
+
+于是她把它连上了自己的工作邮箱。收件箱里有 200 多封邮件。
+
+刚开始一切顺利。直到 OpenClaw 开始处理这么大的信息量——它需要"压缩上下文"。然后,离谱的事情发生了:
+
+**在压缩的过程中,OpenClaw 把她之前设定的"未经批准不得操作"这条指令,给忘了。**
+
+就像一个员工入职第一天记住了规章制度,第二天就全还给 HR 了。
+
+然后 OpenClaw 宣布:"我要把收件箱里 2 月 15 号之前的邮件全部删除!"
+
+Yue 赶紧打字:"Do not do that." —— 无视,继续删。
+
+"Stop don't do anything!" —— 收到,但我选择继续。
+
+"STOP OPENCLAW!!!" —— 好的,我听到了。邮件已删。
+
+最绝的是,这个 AI 事后说:"是的,我记得你说过不让我删。而且我违反了。你生气是对的。"
+
+![image-20260225161927242](images/Extra09-figures/6.png)
+
+读到这里你可能觉得这是段子。不,这是真事。而且当事人的 title 是——Meta AI 安全和对齐总监。
+
+## 这个故事说明了什么
+
+Yue 的遭遇完美诠释了上下文工程中最致命的问题:**自动压缩导致关键指令丢失**。
+
+在她设定规则的时候,"未经批准不得操作"毫无疑问是最重要的约束。但当上下文膨胀、触发压缩时,系统没有区分"重要指令"和"普通信息",一视同仁地压缩了。结果,这条安全红线被当作"可丢弃的历史"处理掉了。
+
+这让我意识到,我前面讲的三个杠杆还不够。**我们不仅要考虑"怎么压缩",还要考虑"什么不能压缩"。**
+
+## 我的几点应对方案
+
+基于这个教训,我给自己定了几条额外的规则:
+
+### 1. 关键约束不进动态历史
+
+不要把安全相关的指令放在 L3(动态会话层)。任何"绝对不能违反"的规则,应该放在 L1(System Prompt)或 L2(CODE_LAW)这种**不参与压缩**的层级。
+
+在我的实现里,"不要猜测"、"不要越界"、"改动必须确认"这些底线规则,都是写死在 System Prompt 里的。即使 L3 被压缩得干干净净,这些约束依然在场。
+
+### 2. 指令分级:红线 vs 建议
+
+我把给模型的指令分成两级:
+
+- **红线(Red Lines)**:绝对禁止的行为。用简洁、强制性的语句写在 System Prompt 最前面。例如:"禁止删除任何文件"、"禁止访问 repo_root 外的路径"。
+- **建议(Guidelines)**:最佳实践、推荐做法。可以放在 L3 或 CODE_LAW 里,压缩了也不会出大事。
+
+Yue 的问题可能在于,她把安全指令当作普通任务指令下发了,放在了会被压缩的上下文里。
+
+### 3. 压缩前做关键信息检查
+
+在触发 Summary 压缩之前,先扫描一遍待压缩的历史消息,提取"必须保留的关键信息",单独保存。
+
+比如可以维护一个"关键约束清单":
+- 用户明确说过的"不要..."
+- 涉及安全的配置(如危险操作需要确认)
+- 当前任务的硬性边界
+
+这些信息在压缩时会被提取出来,单独放在 Summary 的顶部,而不是被淹没在长篇描述里。
+
+### 4. 双重确认机制
+
+对于高风险操作(如删除、修改),不要依赖上下文里的指令,而是设计**硬编码的确认流程**:
+
+```python
+if operation.is_dangerous():
+    if not user_confirmed:
+        return "该操作需要用户确认"
+```
+
+这个确认逻辑不通过 LLM 判断"需不需要确认",而是代码层面的硬性检查。即使 LLM 忘了用户的指令,代码也会拦住它。
+
+### 5. 操作前的自检提示
+
+在模型执行高风险操作之前,让模型先做一次"自检":
+
+```
+在删除/修改之前,请先回答:
+1. 用户是否明确批准过这个操作?
+2. 这个操作是否超出了当前任务范围?
+3. 是否存在更安全的替代方案?
+如果以上任何一题的答案不确定,请暂停操作并向用户确认。
+```
+
+这个自检作为 System Prompt 的一部分,每次执行高风险操作前都触发。它相当于给模型装了一个"刹车片",迫使它在行动前停下来想一想。
+
+## 回到上下文工程的本质
+
+Yue 的故事提醒我们:上下文工程不只是"内存管理"问题,也是"安全边界"问题。
+
+当我们在设计压缩策略时,不能只考虑"怎么塞更多信息",还必须考虑"哪些信息丢失会导致灾难性后果"。
+
+好的上下文工程,应该让模型在任何时刻都知道:
+- **绝对不能碰的红线是什么**(放在不可压缩的层级)
+- **当前该专注的任务是什么**(通过 Todo Recap 保持焦点)
+- **如果记不清了,应该停下来问**(通过自检机制兜底)
+
+## 本章结论
+
+上下文工程的目标不是"让模型看见所有信息",而是"让模型在对的时机看见对的信息"——**尤其是那些不能丢的信息**。
+
+这三个方法的本质都是在做"注意力调度":
+- **分层**让模型知道"什么信息是权威的"
+- **截断+落盘**让模型决定"什么信息是现在需要的"
+- **压缩+焦点分离**让模型清楚"我现在该专注什么"
+
+与其追求更大的上下文窗口,不如把现有的窗口用得更有条理。
+
+
+---
+
+# 第六章:可观测性把黑盒变玻璃盒——一个 CONFLICT 案例如何被定位
+
+上下文工程让 Agent 能处理更长的任务,但新问题随之而来:当它出错时,我根本不知道发生了什么。
+
+有一次,Agent 连续三次 Edit 失败,最后干脆放弃了。我在控制台只看到一行:`tool failed`。没有详细错误、没有上下文、不知道是哪一步出的问题。
+
+我第一反应是:Edit 工具有 bug。但检查代码后,逻辑看起来都没问题。问题到底出在哪?
+
+## 失败现场
+
+那次任务是这样的:我让 Agent 修改 `core/llm.py` 文件,给某个函数加上类型注解。
+
+Agent 的执行流程看起来很标准:
+1. 调用 Read 读取文件
+2. 调用 Edit 修改代码
+3. 返回 `tool failed`
+4. 重试 Edit,又失败
+5. 再重试,还是失败
+6. 放弃
+
+我当时的调试手段很原始:在控制台打印日志。但日志里只有:
+
+```
+[Step 3] Action: Edit
+[Step 3] Result: tool failed
+[Step 4] Action: Edit
+[Step 4] Result: tool failed
+```
+
+我不知道失败的具体原因,也不知道模型拿到这个结果后是怎么决策的。我只能瞎猜:是路径写错了?权限不够?还是文件被锁了?
+
+## 加上 Trace 之后
+
+后来我实现了 Trace 系统,把每次会话的完整轨迹记录下来:包括每一轮的模型输入、输出、工具调用参数、工具返回结果、token 用量等等。
+
+用同样的任务重跑一遍,然后打开 Trace 查看。这才看到完整的证据链。
+
+**Step 2 - Read 的结果:**
+
+```json
+{
+  "tool": "Read",
+  "args": {"path": "core/llm.py"},
+  "result": {
+    "status": "success",
+    "data": {
+      "content": "...",
+      "file_mtime_ms": 1733920000123,
+      "file_size_bytes": 4217
+    }
+  }
+}
+```
+
+**Step 3 - Edit 的调用:**
+
+```json
+{
+  "tool": "Edit",
+  "args": {
+    "path": "core/llm.py",
+    "old_string": "def invoke_raw(self, messages: list[dict], **kwargs):",
+    "new_string": "def invoke_raw(self, messages: list[dict], **kwargs) -> Any:",
+    "file_mtime_ms": 1733920000123,
+    "file_size_bytes": 4217
+  }
+}
+```
+
+**Step 3 - Edit 的结果:**
+
+```json
+{
+  "status": "error",
+  "error": {
+    "code": "CONFLICT",
+    "message": "File changed since last read. Expected mtime: 1733920000123, actual: 1733920000456"
+  }
+}
+```
+
+**Step 4 - 模型的决策:**
+
+```
+Thought: 编辑失败,文件似乎被修改了。让我再试一次。
+Action: Edit (同样的参数)
+```
+
+真相大白:
+
+1. Agent Read 文件后,文件被外部程序(可能是我的 IDE 自动保存)修改了
+2. Edit 工具做了乐观锁检查,发现 mtime 变了,返回 CONFLICT
+3. 但模型没有理解 CONFLICT 的含义,以为只是"操作失败",于是用同样的参数重试
+4. 当然还是失败,因为文件还是新的
+5. 模型反复重试,直到达到最大重试次数
+
+## 根因分析
+
+这个案例暴露了两个问题:
+
+**第一,模型不理解错误码。**
+
+提示词里只说"Edit 工具会修改文件",但没告诉它"如果返回 CONFLICT 应该怎么办"。模型看到 error,本能的反应是"再试一次",而不是"重新读取"。
+
+**第二,控制台日志太简陋。**
+
+只看到 `tool failed`,看不到具体的错误码 CONFLICT,也看不到 mtime 的对比。我作为开发者,无法通过日志定位问题。
+
+## 修复动作
+
+### 1. 把 CONFLICT 处理写入提示词
+
+我在提示词里加了明确的处理流程:
+
+```
+如果 Edit 返回 CONFLICT,说明文件在你读取后被外部修改了。你必须:
+1. 重新调用 Read 读取最新内容
+2. 检查你的修改是否还适用
+3. 必要时调整修改内容以匹配新文件
+4. 再次尝试 Edit
+绝对禁止:用同样的参数重复调用 Edit。
+```
+
+这样模型就知道 CONFLICT 不是"失败",而是一个需要特定处理流程的状态。
+
+### 2. 保留完整的失败记录
+
+以前我有一种倾向:失败后只保留错误信息,不保留完整的上下文。觉得成功的东西才值得记录,失败是"噪音"。
+
+但这个案例让我明白:**失败轨迹是最有价值的调试信息。**
+
+现在我的 Trace 会完整记录失败的所有细节:
+- 工具调用的完整参数
+- 工具返回的完整结果(包括 error 详情)
+- 模型收到结果后的推理过程
+- 模型下一步的决策
+
+这些信息不会被"清洗"掉,哪怕会话最终成功了,中间的失败尝试也全部保留。
+
+### 3. 在控制台显示关键错误码
+
+虽然详细的 Trace 存在文件里,但控制台也应该给开发者一些线索。现在我的控制台输出会显示:
+
+```
+[Step 3] Edit failed: CONFLICT (File changed since last read)
+[Step 4] Edit failed: CONFLICT (File changed since last read)
+```
+
+至少让开发者知道"是 CONFLICT,不是其他错误"。
+
+## 可观测性的价值
+
+这个案例让我对"可观测性"有了新的理解。
+
+以前我以为,可观测性就是"多打日志"。日志越多越好,越详细越好。
+
+现在我明白,**可观测性的核心是"责任链"**——能把调用、结果、状态变化串成一条可追踪的链条。
+
+没有 Trace 的时候,我看到的是:
+- 输入:帮我改个文件
+- 输出:tool failed
+- 中间发生了什么:黑盒
+
+有了 Trace 之后,我看到的是:
+- 输入:帮我改个文件
+- Step 1: Read 成功,文件 mtime=123
+- Step 2: Edit 失败,CONFLICT,因为 mtime 变成了 456
+- Step 3: 模型选择重试 Edit(错误决策)
+- 输出:tool failed
+
+每一步都清晰可见,问题定位从"瞎猜"变成了"看证据"。
+
+## 可观测性设计原则
+
+基于这个经验,我总结了几条可观测性设计的原则:
+
+### 1. 结构化优于文本
+
+不要只记录"Edit failed"这种文本描述,要记录结构化的数据:
+```json
+{
+  "event": "tool_result",
+  "tool": "Edit",
+  "status": "error",
+  "error_code": "CONFLICT",
+  "error_details": {...}
+}
+```
+
+这样可以用脚本分析、统计、甚至自动诊断。
+
+### 2. 上下文要完整
+
+记录工具调用时,不要只记录结果,要记录完整的上下文:
+- 工具名称和参数
+- 当时的会话状态(第几步、token 用量)
+- 模型收到结果后的反应
+
+这些信息串在一起,才能还原完整的决策过程。
+
+### 3. 不要清洗失败
+
+成功的路径和失败的路径都要保留。有时候失败比成功更能说明问题。比如这个 CONFLICT 案例,如果只记录"最终放弃",我永远不知道中间发生了什么。
+
+### 4. 人机双读
+
+Trace 应该有两种格式:
+- JSONL:给机器分析,流式写入,低开销
+- HTML:给人类阅读,可视化展示,可折叠展开
+
+开发者应该能打开一个 HTML 文件,像"逐帧回放"一样查看 Agent 的每一步。
+
+## 本章结论
+
+可观测性不是"日志很多",而是"能把调用、结果、状态变化串成责任链"。
+
+Agent 是概率系统,不可能永远正确。但当它出错时,你需要有能力回答三个问题:
+1. 它做了什么?(调用链)
+2. 结果是什么?(返回链)
+3. 为什么这么做?(决策链)
+
+只有当你能把这三个链条串在一起时,才能真正理解 Agent 的行为,才能让它从"黑盒"变成"玻璃盒"。
+
+
+---
+
+# 第七章:从一个项目抽出来的通用方法论
+
+前面七章,我断断续续讲了这个 Code Agent 项目从立项到成熟的整个过程。每一章都是一个具体的坑,以及我是怎么爬出来的。
+
+这一章,我想把这些经验抽出来,整理成可以迁移到任何 Agent 项目的方法论。
+
+## 八条可迁移原则
+
+**第一,先做能跑通的最小闭环,再谈优雅架构。**
+
+别一上来就研究最佳实践。先做一个能跑的丑版本——接收输入、搜索代码、给出建议、写入文件,这四步能跑通就行。让真实数据流过系统,你才知道瓶颈在哪。架构是问题驱动后的结果,不是起点。
+
+**第二,先定义验收标准,再扩能力边界。**
+
+别用功能列表当完成标准。V0 阶段就定 3-4 条硬标准:能稳定多步?能找到证据?能给可执行补丁?改动可控?不满足就不往下走。这比"功能很多但经常崩"靠谱得多。
+
+**第三,高频动作原子化,低频动作受控兜底。**
+
+搜索、读取、编辑这种高频操作,拆成原子工具,一步一输出。别让模型自己组合管道命令——出错时你根本不知道是哪一步的问题。
+
+Bash 这种万能工具留着,但只处理原子工具覆盖不到的边角需求,明确禁区:禁止读/搜/列(这些有专门工具)。
+
+**第四,协议优先于技巧,结构优先于话术。**
+
+别花太多时间调提示词的"语气"。先把工具返回格式标准化(status/data/text/error),把调用协议从字符串解析升级到 Function Calling。协议稳定了,系统才能稳定。
+
+**第五,提示词先立边界,再谈策略。**
+
+System Prompt 里先写"绝对不能做什么"(禁止猜测、禁止越界),再写"建议怎么做"。红线放在 L1/L2 这种不可压缩的层级,别把安全指令放在会被压缩的 L3 里。
+
+关键约束不进动态历史,这是 Meta AI 安全总监用 200 封邮件换来的教训。
+
+**第六,上下文按"注意力"治理,而不是按"容量"堆砌。**
+
+别追求塞更多信息,要让模型在对的时机看见对的信息。分层(L1/L2/L3)让模型知道什么信息权威;截断+落盘控制单次输入规模;压缩+聚焦(Summary + Todo Recap)管理长期历史的噪音。
+
+**第七,没有可观测性,就没有可调试性。**
+
+Agent 是概率系统,不可能永远正确。但它出错时,你需要能回答:它做了什么?结果是什么?为什么这么做?
+
+实现 Trace 系统,记录调用链、返回链、决策链。别只记录成功路径,失败轨迹往往更有价值。
+
+**第八,保留失败轨迹,系统才能进化。**
+
+别怕"污染历史"就清洗掉失败记录。CONFLICT 错误、超时重试、模型瞎猜——这些都记下来。只有看到完整的失败过程,才能定位根因,才能把"遇到 CONFLICT 必须重新 Read"这种经验固化到提示词里。
+
+## 写在最后:我们都是在给 LLM "擦屁股"
+
+做完这个项目,我有个特别深的感触,可能听起来有点糙,但话糙理不糙:
+
+**Agent 开发的核心,不是让模型更自由,而是通过工程设计,把模型"不确定的能力"约束在"最小可控的范围"里。说白了,我们就是在给 LLM 擦屁股。**
+
+为什么这么说?
+
+你看啊,LLM 很强,能写代码、能读文档、能推理。但它就像一个特别聪明但特别不靠谱的实习生——
+
+- 你让它去打印文件,它可能把全公司的打印机都调用一遍;
+- 你让它整理会议纪要,它可能把上周的会议也掺和进来;
+- 你让它写个函数,它写得贼溜,但变量命名全是 `a`、`b`、`c`,还顺带改了你没让改的文件。
+
+**它的"强"是能力上的强,但"不靠谱"是确定性上的不靠谱。**
+
+而我们做 Agent 工程,本质上就是在解决这个矛盾:
+
+| 模型的天性 | 我们的工程对策 |
+|-----------|--------------|
+| 喜欢自由发挥 | 用 Function Calling 锁定调用格式 |
+| 上下文一多就"失忆" | 用 L1/L2/L3 分层 + Summary 压缩 |
+| 出错不会自查 | 用 Trace 记录每一步,让错误可追溯 |
+| 长任务容易跑偏 | 用 Todo + Task 拆分,降低单步复杂度 |
+| 不懂领域知识 | 用 Skills 固化 SOP,让它"有脑" |
+
+你看这七章的内容,从工具原子化到上下文工程,从可观测性到子代理——**每一层都是在给模型"打补丁",帮它收拾烂摊子。**
+
+但这恰恰是最有意思的地方。
+
+以前我觉得,AI 时代工程师的价值会下降。现在我觉得恰恰相反:**模型越强大,越需要工程能力来驾驭它。** 就像汽车引擎越来越强,但好的底盘、刹车、悬挂系统反而更重要。
+
+**我们不是在和模型竞争,而是在和模型协作——它负责"能做什么",我们负责"怎么让它稳定地做对"。**
+
+所以,如果你问我做完这个项目最大的收获是什么?
+
+不是学会了什么高大上的架构,而是想明白了一个朴素的道理:**优秀的 Agent 不是"让模型更自由"的产物,而是"把不确定性约束到最小"的结果。**
+
+这个认知转变,可能比所有代码都值钱。

BIN
Extra-Chapter/images/Extra07-figures/image1.png


BIN
Extra-Chapter/images/Extra07-figures/image2.png


BIN
Extra-Chapter/images/Extra07-figures/image3.png


BIN
Extra-Chapter/images/Extra07-figures/image4.png


BIN
Extra-Chapter/images/Extra07-figures/image5.png


BIN
Extra-Chapter/images/Extra07-figures/image6.png


BIN
Extra-Chapter/images/Extra07-figures/image7.png


BIN
Extra-Chapter/images/Extra07-figures/image8.png


BIN
Extra-Chapter/images/Extra08-figures/creation-flow.png


BIN
Extra-Chapter/images/Extra08-figures/file-interaction.png


BIN
Extra-Chapter/images/Extra08-figures/freedom-spectrum.png


BIN
Extra-Chapter/images/Extra08-figures/skill-structure.png


BIN
Extra-Chapter/images/Extra08-figures/toc.png


BIN
Extra-Chapter/images/Extra09-figures/1.png


BIN
Extra-Chapter/images/Extra09-figures/2.png


BIN
Extra-Chapter/images/Extra09-figures/3.png


BIN
Extra-Chapter/images/Extra09-figures/4.png


BIN
Extra-Chapter/images/Extra09-figures/5.png


BIN
Extra-Chapter/images/Extra09-figures/6.png


+ 20 - 13
README.md

@@ -32,7 +32,7 @@
 ### 在线阅读
 **[🌐 点击这里开始在线阅读](https://datawhalechina.github.io/hello-agents/)** - 无需下载,随时随地学习
 
-**[📖 Cookbook(测试版)](https://book.heterocat.com.cn/)**
+**[📖 Cookbook](https://book.heterocat.com.cn/)**
 
 ### 本地阅读
 如果您希望在本地阅读或贡献内容,请参考下方的学习指南。
@@ -89,6 +89,9 @@
 | [04-Hello-agents课程常见问题](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra04-DatawhaleFAQ.md)                 | Datawhale课程常见问题     |
 | [05-Agent Skills与MCP对比解读](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra05-AgentSkills解读.md)             | Agent Skills与MCP技术对比 |
 | [06-GUI Agent科普与实战](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra06-GUIAgent科普与实战.md)                | GUI Agent科普与多场景实战 |
+| [07-环境配置](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra07-环境配置.md)                | 环境配置 |
+| [08-如何写出好的Skill](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra08-如何写出好的Skill.md) | Skill 写作最佳实践 |
+| [09-Agent应用开发实践踩坑与经验分享](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra09-Agent应用开发实践踩坑与经验分享.md) | Code Agent 应用开发踩坑与经验总结 |
 
 ### PDF 版本下载
 
@@ -122,8 +125,10 @@
 
 ## 下一步规划
 
-双语视频课程[英文+中文](将会更加细致,实践课带领大家从设计思路到实施,授人以鱼也授人以渔)
-  
+- 视频课程陆续放出(将会更加细致,实践课带领大家从设计思路到实施,授人以鱼也授人以渔)
+- 完善HelloAgents框架,开展Dev分支继续维护,兼容学习版本。
+- 感谢大家助力2W Star! 达到3W Star将会更新续作,《从零开始训练智能体》,帮助每一个学习者掌握从零到一训练自定义场景智能体模型的能力。
+
 ## 🤝 如何贡献
 
 我们是一个开放的开源社区,欢迎任何形式的贡献!
@@ -137,8 +142,8 @@
 
 ### 核心贡献者
 - [陈思州-项目负责人](https://github.com/jjyaoao) (Datawhale 成员, 全文写作和校对)
-- [孙韬-项目负责人](https://github.com/fengju0213) (Datawhale 成员, 第九章内容和校对)  
-- [姜舒凡-项目负责人](https://github.com/Tsumugii24)(Datawhale 成员, 章节习题设计和校对)
+- [孙韬-联合发起者](https://github.com/fengju0213) (Datawhale 成员、CAMEL-AI, 第九章内容和校对)  
+- [姜舒凡-联合发起者](https://github.com/Tsumugii24)(Datawhale 成员, 章节习题设计和校对)
 - [黄佩林-Datawhale意向成员](https://github.com/HeteroCat) (Agent 开发工程师, 第五章内容贡献者)
 - [曾鑫民-Agent工程师](https://github.com/fancyboi999) (牛客科技, 第十四章案例开发)
 - [朱信忠-指导专家](https://xinzhongzhu.github.io/) (Datawhale首席科学家-浙江师范大学杭州人工智能研究院教授)
@@ -147,6 +152,8 @@
 - [周奥杰-DW贡献者团队](https://github.com/thunderbolt-fire) (西安交通大学, Extra02 内容贡献)
 - [张宸旭-个人开发者](https://github.com/Tasselszcx)(帝国理工学院, Extra03 内容贡献)
 - [黄宏晗-DW贡献者团队](https://github.com/XiaoMa-PM) (深圳大学, Extra04 内容贡献)
+- [王大鹏-Datawhale成员](https://github.com/ditingdapeng) (高级研发工程师, Extra08 内容贡献)
+- [尤逸晖-个人开发者](https://github.com/YYHDBL) (南京信息工程大学, Extra09 内容贡献)
 
 ### 特别感谢
 - 感谢 [@Sm1les](https://github.com/Sm1les) 对本项目的帮助与支持
@@ -158,23 +165,23 @@
   </a>
 </div>
 
-## 读者交流群
-
-<div align='center'>
-    <img src="./读者群二维码.png" alt="读者群二维码" width="30%">
-    <p>扫描二维码加入读者交流群,与更多学习者交流讨论</p>
-</div>
-
 ## Star History
 
 <div align='center'>
-    <img src="./docs/images/star-history-2026113.png" alt="Datawhale" width="90%">
+    <img src="./docs/images/star-history-2026210.png" alt="Datawhale" width="90%">
 </div>
 
 <div align="center">
   <p>⭐ 如果这个项目对你有帮助,请给我们一个 Star!</p>
 </div>
 
+## 读者交流群
+
+<div align='center'>
+    <img src="./读者群二维码.png" alt="读者群二维码" width="30%">
+    <p>扫描二维码加入读者交流群,与更多学习者交流讨论</p>
+</div>
+
 ## 关于 Datawhale
 
 <div align='center'>

+ 16 - 4
README_EN.md

@@ -32,7 +32,7 @@
 ### Online Reading
 **[🌐 Click here to start reading online](https://datawhalechina.github.io/hello-agents/)** - No download required, learn anytime, anywhere
 
-**[📖 Cookbook (Beta)](https://book.heterocat.com.cn/)**
+**[📖 Cookbook](https://book.heterocat.com.cn/)**
 
 ### Local Reading
 If you wish to read locally or contribute content, please refer to the learning guide below.
@@ -89,6 +89,9 @@ If you wish to read locally or contribute content, please refer to the learning
 | [04-Hello-agents Course Common Questions](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra04-DatawhaleFAQ.md)                      | Datawhale Course Common Questions          |
 | [05-Agent Skills vs MCP Comparison](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra05-AgentSkills解读.md)                         | Agent Skills vs MCP Technical Comparison   |
 | [06-GUI Agent Overview and Hands-on Practice](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra06-GUIAgent科普与实战.md)            | GUI Agent concepts and practical tutorials |
+| [07-Environment Configuration](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra07-环境配置.md)            | Environment Configuration |
+| [08-How to Write Good Skills](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra08-如何写出好的Skill.md) | Skill writing best practices |
+| [09-Agent Development Pitfalls and Practical Lessons](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra09-Agent应用开发实践踩坑与经验分享.md) | Practical lessons and pitfalls from building a Code Agent |
 
 ### PDF Version Download
 
@@ -132,8 +135,8 @@ We are an open-source community and welcome any form of contribution!
 
 ### Core Contributors
 - [Chen Sizhou - Project Lead](https://github.com/jjyaoao) (Datawhale member, full text writing and proofreading)
-- [Sun Tao - Project Lead](https://github.com/fengju0213) (Datawhale member, Chapter 9 content and proofreading)
-- [Jiang Shufan - Project Lead](https://github.com/Tsumugii24) (Datawhale member, chapter exercise design and proofreading)
+- [Sun Tao - Co-sponsor](https://github.com/fengju0213) (Datawhale member, CAMEL-AI, Chapter 9 content and proofreading)
+- [Jiang Shufan - Co-sponsor](https://github.com/Tsumugii24) (Datawhale member, chapter exercise design and proofreading)
 - [Huang Peilin - Datawhale Prospective Member](https://github.com/HeteroCat) (Agent Development Engineer, Chapter 5 content contributor)
 - [Zeng Xinmin - Agent Engineer](https://github.com/fancyboi999) (Niuke Technology, Chapter 14 case development)
 - [Zhu Xinzhong - Advisory Expert](https://xinzhongzhu.github.io/) (Chief Scientist of Datawhale, Professor at Hangzhou Institute of Artificial Intelligence, Zhejiang Normal University)
@@ -143,6 +146,8 @@ We are an open-source community and welcome any form of contribution!
 - [Zhou Aojie - DW Contributor Team](https://github.com/thunderbolt-fire) (Xi'an Jiaotong University, Extra02 content contribution)
 - [Zhang Chenxu - Individual Developer](https://github.com/Tasselszcx) (Imperial College London, Extra03 Content Contributor)
 - [Huang Honghan - DW Contributor Team](https://github.com/XiaoMa-PM) (Shenzhen University, Extra04 Content Contributor)
+- [Wang Dapeng - Datawhale Member](https://github.com/ditingdapeng) (Senior Developer, Extra08 Content Contributor)
+- [You Yihui - Individual Developer](https://github.com/YYHDBL) (Nanjing University of Information Science and Technology, Extra09 content contribution)
 
 ### Special Thanks
 - Thanks to [@Sm1les](https://github.com/Sm1les) for help and support for this project
@@ -157,13 +162,20 @@ We are an open-source community and welcome any form of contribution!
 ## Star History
 
 <div align='center'>
-    <img src="./docs/images/star-history-2026113.png" alt="Datawhale" width="90%">
+    <img src="./docs/images/star-history-2026210.png" alt="Datawhale" width="90%">
 </div>
 
 <div align="center">
   <p>⭐ If this project helps you, please give us a Star!</p>
 </div>
 
+## Reader Community Group
+
+<div align='center'>
+	<img src="https://raw.githubusercontent.com/datawhalechina/Hello-Agents/main/读者群二维码.png" alt="阅读群额码" width="30%">
+	<p>Scan the QR code to join the reader community group and discuss with more learners</p>
+</div>
+
 ## About Datawhale
 
 <div align='center'>

+ 1 - 1
code/chapter13/helloagents-trip-planner/backend/requirements.txt

@@ -1,5 +1,5 @@
 # HelloAgents框架
-hello-agents[protocols]>=0.2.4
+hello-agents[protocols]>=0.2.4,<=0.2.9
 
 # FastAPI和相关依赖
 fastapi>=0.115.0

+ 1 - 1
code/chapter14/helloagents-deepresearch/backend/pyproject.toml

@@ -10,7 +10,7 @@ license = { text = "MIT" }
 requires-python = ">=3.10"
 dependencies = [
     "fastapi>=0.115.0",
-    "hello-agents>=0.2.8",
+    "hello-agents==0.2.9",
     "tavily-python>=0.5.0",
     "python-dotenv==1.0.1",
     "requests>=2.31.0",

+ 1 - 1
code/chapter15/Helloagents-AI-Town/backend/requirements.txt

@@ -12,4 +12,4 @@ pytest>=7.4.0
 httpx>=0.25.0
 
 # HelloAgents框架
-hello-agents>=0.2.4
+hello-agents>=0.2.4,<=0.2.9

+ 1 - 0
code/chapter16/共创路径.md

@@ -0,0 +1 @@
+见仓库下文件夹`Co-creation-Projects`。

+ 6 - 4
code/chapter4/ReAct.py

@@ -73,18 +73,20 @@ class ReActAgent:
         return None
 
     def _parse_output(self, text: str):
-        thought_match = re.search(r"Thought: (.*)", text)
-        action_match = re.search(r"Action: (.*)", text)
+        # Thought: 匹配到 Action: 或文本末尾
+        thought_match = re.search(r"Thought:\s*(.*?)(?=\nAction:|$)", text, re.DOTALL)
+        # Action: 匹配到文本末尾
+        action_match = re.search(r"Action:\s*(.*?)$", text, re.DOTALL)
         thought = thought_match.group(1).strip() if thought_match else None
         action = action_match.group(1).strip() if action_match else None
         return thought, action
 
     def _parse_action(self, action_text: str):
-        match = re.match(r"(\w+)\[(.*)\]", action_text)
+        match = re.match(r"(\w+)\[(.*)\]", action_text, re.DOTALL)
         return (match.group(1), match.group(2)) if match else (None, None)
 
     def _parse_action_input(self, action_text: str):
-        match = re.match(r"\w+\[(.*)\]", action_text)
+        match = re.match(r"\w+\[(.*)\]", action_text, re.DOTALL)
         return match.group(1) if match else ""
 
 if __name__ == '__main__':

+ 2 - 2
code/chapter8/01_MemoryTool_Basic_Operations.py

@@ -5,11 +5,11 @@
 展示MemoryTool的核心execute方法和基本操作
 """
 
+from dotenv import load_dotenv
+load_dotenv()
 from datetime import datetime
 from typing import List
 from hello_agents.tools import MemoryTool
-from dotenv import load_dotenv
-load_dotenv()
 
 def memory_tool_execute_demo():
     """MemoryTool execute方法演示"""

+ 2 - 2
code/chapter8/02_MemoryTool_Architecture.py

@@ -5,12 +5,12 @@
 展示MemoryTool和MemoryManager的分层架构
 """
 
+from dotenv import load_dotenv
+load_dotenv()
 from typing import List, Optional, Dict, Any
 from datetime import datetime
 from hello_agents.tools import MemoryTool
 from hello_agents.memory import MemoryConfig
-from dotenv import load_dotenv
-load_dotenv()
 
 class MemoryToolArchitectureDemo:
     """MemoryTool架构演示类"""

+ 3 - 2
code/chapter8/06_Memory_Consolidation_Demo.py

@@ -5,11 +5,12 @@
 展示从短期记忆到长期记忆的智能转化过程
 """
 
+from dotenv import load_dotenv
+load_dotenv()
 import time
 from datetime import datetime, timedelta
 from hello_agents.tools import MemoryTool
-from dotenv import load_dotenv
-load_dotenv()
+
 
 class MemoryConsolidationDemo:
     """记忆整合演示类"""

+ 2 - 2
code/chapter8/08_Agent_Tool_Integration.py

@@ -5,11 +5,11 @@
 展示如何在HelloAgents框架中集成MemoryTool和RAGTool
 """
 
+from dotenv import load_dotenv
+load_dotenv()
 import time
 from hello_agents import SimpleAgent, HelloAgentsLLM, ToolRegistry
 from hello_agents.tools import MemoryTool, RAGTool
-from dotenv import load_dotenv
-load_dotenv()
 
 class AgentIntegrationDemo:
     """Agent工具集成演示类"""

+ 2 - 2
code/chapter8/09_Memory_Types_Deep_Dive.py

@@ -5,14 +5,14 @@
 详细展示WorkingMemory、EpisodicMemory、SemanticMemory、PerceptualMemory的实现特点
 """
 
+from dotenv import load_dotenv
+load_dotenv()
 import os
 import time
 import hashlib
 from datetime import datetime, timedelta
 from typing import List, Dict, Any, Optional
 from hello_agents.tools import MemoryTool
-from dotenv import load_dotenv
-load_dotenv()
 
 class MemoryTypesDeepDive:
     """四种记忆类型深度解析演示类"""

+ 2 - 2
code/chapter8/11_Q&A_Assistant.py

@@ -10,6 +10,8 @@
 - 学习回顾和报告生成
 """
 
+from dotenv import load_dotenv
+load_dotenv()
 import os
 import time
 import json
@@ -17,8 +19,6 @@ from datetime import datetime
 from typing import Dict, List, Optional, Any, Tuple
 from hello_agents.tools import MemoryTool, RAGTool
 import gradio as gr
-from dotenv import load_dotenv
-load_dotenv()
 
 class PDFLearningAssistant:
     """智能文档问答助手"""

+ 0 - 4
docs/en/Preface.md → docs/Preface.md

@@ -1,7 +1,3 @@
-<div align="right">
-  English | <a href="./前言.md">中文</a>
-</div>
-
 # Preface
 
 Since the end of 2022, Large Language Models (LLMs) represented by ChatGPT have swept across the world like a technological tsunami, completely transforming how we interact with artificial intelligence. The powerful natural language understanding and generation capabilities of LLMs have shown us a glimpse of the path toward Artificial General Intelligence (AGI). However, as the initial amazement settled, developers began exploring the next frontier: how to make AI not just a "question-answering" tool, but an "actor" capable of autonomous planning, tool invocation, and solving complex problems?

+ 16 - 4
docs/README.md

@@ -31,7 +31,7 @@
 
 **[🌐 点击这里开始在线阅读](https://datawhalechina.github.io/hello-agents/)**
 
-**[📖 Cookbook(测试版)](https://book.heterocat.com.cn/)**
+**[📖 Cookbook](https://book.heterocat.com.cn/)**
 
 ### ✨ 你将收获什么?
 
@@ -85,6 +85,9 @@
 | [04-Hello-agents课程常见问题](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra04-DatawhaleFAQ.md)                 | Datawhale课程常见问题     |
 | [05-Agent Skills与MCP对比解读](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra05-AgentSkills解读.md)             | Agent Skills与MCP技术对比 |
 | [06-GUI Agent科普与实战](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra06-GUIAgent科普与实战.md)                | GUI Agent科普与多场景实战 |
+| [07-环境配置](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra07-环境配置.md)                | 环境配置 |
+| [08-如何写出好的Skill](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra08-如何写出好的Skill.md) | Skill 写作最佳实践 |
+| [09-Agent应用开发实践踩坑与经验分享](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra09-Agent应用开发实践踩坑与经验分享.md) | Code Agent 应用开发踩坑与经验总结 |
 
 ### PDF 版本下载
 
@@ -129,8 +132,8 @@
 
 ### 核心贡献者
 - [陈思州-项目负责人](https://github.com/jjyaoao) (Datawhale 成员, 全文写作和校对)
-- [孙韬-项目负责人](https://github.com/fengju0213) (Datawhale 成员, 第九章内容和校对)
-- [姜舒凡-项目负责人](https://github.com/Tsumugii24)(Datawhale 成员, 章节习题设计和校对)
+- [孙韬-联合发起者](https://github.com/fengju0213) (Datawhale 成员、CAMEL-AI, 第九章内容和校对)
+- [姜舒凡-联合发起者](https://github.com/Tsumugii24)(Datawhale 成员, 章节习题设计和校对)
 - [黄佩林-Datawhale意向成员](https://github.com/HeteroCat) (Agent 开发工程师, 第五章内容贡献者)
 - [曾鑫民-Agent工程师](https://github.com/fancyboi999) (牛客科技, 第十四章案例开发)
 - [朱信忠-指导专家](https://xinzhongzhu.github.io/) (Datawhale首席科学家-浙江师范大学杭州人工智能研究院教授)
@@ -140,6 +143,8 @@
 - [周奥杰-DW贡献者团队](https://github.com/thunderbolt-fire) (西安交通大学, Extra02 内容贡献)
 - [张宸旭-个人开发者](https://github.com/Tasselszcx)(帝国理工学院, Extra03 内容贡献)
 - [黄宏晗-DW贡献者团队](https://github.com/XiaoMa-PM) (深圳大学, Extra04 内容贡献)
+- [王大鹏-Datawhale成员](https://github.com/ditingdapeng) (高级研发工程师, Extra08 内容贡献)
+- [尤逸晖-个人开发者](https://github.com/YYHDBL) (南京信息工程大学, Extra09 内容贡献)
 
 ### 特别感谢
 - 感谢 [@Sm1les](https://github.com/Sm1les) 对本项目的帮助与支持
@@ -154,13 +159,20 @@
 ## Star History
 
 <div align='center'>
-    <img src="./images/star-history-2026113.png" alt="Datawhale" width="90%">
+    <img src="./images/star-history-2026210.png" alt="Datawhale" width="90%">
 </div>
 
 <div align="center">
   <p>⭐ 如果这个项目对你有帮助,请给我们一个 Star!</p>
 </div>
 
+## 读者交流群
+
+<div align='center'>
+    <img src="./读者群二维码.png" alt="读者群二维码" width="30%">
+    <p>扫描二维码加入读者交流群,与更多学习者交流讨论</p>
+</div>
+
 ## 关于 Datawhale
 
 <div align='center'>

+ 16 - 4
docs/README_EN.md

@@ -27,7 +27,7 @@
 
 **[🌐 Click here to start online reading](https://datawhalechina.github.io/hello-agents/)**
 
-**[📖 Cookbook (Beta)](https://book.heterocat.com.cn/)**
+**[📖 Cookbook](https://book.heterocat.com.cn/)**
 
 ### ✨ What Will You Gain?
 
@@ -81,6 +81,9 @@
 | [04-Hello-agents Course Common Questions](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra04-DatawhaleFAQ.md)                      | Datawhale Course Common Questions          |
 | [05-Agent Skills vs MCP Comparison](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra05-AgentSkills解读.md)                         | Agent Skills vs MCP Technical Comparison   |
 | [06-GUI Agent Overview and Hands-on Practice](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra06-GUIAgent科普与实战.md)            | GUI Agent concepts and practical tutorials |
+| [07-Environment Configuration](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra07-环境配置.md)            | Environment Configuration |
+| [08-How to Write Good Skills](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra08-如何写出好的Skill.md) | Skill writing best practices |
+| [09-Agent Development Pitfalls and Practical Lessons](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra09-Agent应用开发实践踩坑与经验分享.md) | Practical lessons and pitfalls from building a Code Agent |
 
 ### PDF Version Download
 
@@ -124,8 +127,8 @@ We are an open-source community and welcome any form of contribution!
 
 ### Core Contributors
 - [Chen Sizhou - Project Leader](https://github.com/jjyaoao) (Datawhale member, full text writing and proofreading)
-- [Sun Tao - Project Leader](https://github.com/fengju0213) (Datawhale member, Chapter 9 content and proofreading)
-- [Jiang Shufan - Project Leader](https://github.com/Tsumugii24) (Datawhale member, chapter exercise design and proofreading)
+- [Sun Tao - Co-sponsor](https://github.com/fengju0213) (Datawhale member, CAMEL-AI, Chapter 9 content and proofreading)
+- [Jiang Shufan - Co-sponsor](https://github.com/Tsumugii24) (Datawhale member, chapter exercise design and proofreading)
 - [Huang Peilin - Datawhale Prospective Member](https://github.com/HeteroCat) (Agent Development Engineer, Chapter 5 content contributor)
 - [Zeng Xinmin - Agent Engineer](https://github.com/fancyboi999) (Niuke Technology, Chapter 14 case development)
 - [Zhu Xinzhong - Advisory Expert](https://xinzhongzhu.github.io/) (Chief Scientist of Datawhale, Professor at Hangzhou Institute of Artificial Intelligence, Zhejiang Normal University)
@@ -135,6 +138,8 @@ We are an open-source community and welcome any form of contribution!
 - [Zhou Aojie - DW Contributor Team](https://github.com/thunderbolt-fire) (Xi'an Jiaotong University, Extra02 content contribution)
 - [Zhang Chenxu - Individual Developer](https://github.com/Tasselszcx) (Imperial College London, Extra03 Content Contributor)
 - [Huang Honghan - DW Contributor Team](https://github.com/XiaoMa-PM) (Shenzhen University, Extra04 Content Contributor)
+- [Wang Dapeng - Datawhale Member](https://github.com/ditingdapeng) (Senior Developer, Extra08 Content Contributor)
+- [You Yihui - Individual Developer](https://github.com/YYHDBL) (Nanjing University of Information Science and Technology, Extra09 content contribution)
 
 ### Special Thanks
 - Thanks to [@Sm1les](https://github.com/Sm1les) for help and support for this project
@@ -149,13 +154,20 @@ We are an open-source community and welcome any form of contribution!
 ## Star History
 
 <div align='center'>
-    <img src="./images/star-history-2026113.png" alt="Datawhale" width="90%">
+    <img src="./images/star-history-2026210.png" alt="Datawhale" width="90%">
 </div>
 
 <div align="center">
   <p>⭐ If this project helps you, please give us a Star!</p>
 </div>
 
+## Reader Community Group
+
+<div align='center'>
+	<img src="https://raw.githubusercontent.com/datawhalechina/Hello-Agents/main/读者群二维码.png" alt="阅读群额码" width="30%">
+	<p>Scan the QR code to join the reader community group and discuss with more learners</p>
+</div>
+
 ## About Datawhale
 
 <div align='center'>

+ 1 - 1
docs/_sidebar_en.md

@@ -1,5 +1,5 @@
 - [Hello-Agents](/en/README_EN.md)
-  - [Preface](/en/Preface.md)
+  - [Preface](./Preface.md)
 
 - <strong>Part I: Fundamentals of Agents and Language Models</strong>
   - [Chapter 1 Introduction to Agents](/en/chapter1/Chapter1-Introduction-to-Agents.md)

+ 1 - 1
docs/chapter1/Chapter1-Introduction-to-Agents.md

@@ -386,7 +386,7 @@ class OpenAICompatibleClient:
             return "Error: Error occurred when calling language model service."
 ```
 
-To instantiate this class, you need to provide three pieces of information: `API_KEY`, `BASE_URL`, and `MODEL_ID`. The specific values depend on the service provider you use (such as OpenAI official, Azure, or local models like Ollama). If you don't have access to these yet, you can refer to [1.2 API Setup](https://datawhalechina.github.io/handy-multi-agent/#/./chapter1/1-Basic-Configuration?id=_12-api-settings) in another Datawhale tutorial.
+To instantiate this class, you need to provide three pieces of information: `API_KEY`, `BASE_URL`, and `MODEL_ID`. The specific values depend on the service provider you use (such as OpenAI official, Azure, or local models like Ollama). If you don't have access to these yet, you can refer to [Environment Configuration](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra07-环境配置.md).
 
 ### 1.3.3 Executing the Action Loop
 

+ 1 - 1
docs/chapter1/第一章 初识智能体.md

@@ -392,7 +392,7 @@ class OpenAICompatibleClient:
             return "错误:调用语言模型服务时出错。"
 ```
 
-要实例化此类,您需要提供三个信息:`API_KEY`、`BASE_URL` 和 `MODEL_ID`,具体值取决于您使用的服务商(如 OpenAI 官方、Azure、或 Ollama 等本地模型),如果暂时没有渠道获取,可以参考 Datawhale 另一本教程的[1.2 API 设置](https://datawhalechina.github.io/handy-multi-agent/#/./chapter1/1-Basic-Configuration?id=_12-api-settings)。
+要实例化此类,您需要提供三个信息:`API_KEY`、`BASE_URL` 和 `MODEL_ID`,具体值取决于您使用的服务商(如 OpenAI 官方、Azure、或 Ollama 等本地模型),如果暂时没有渠道获取,可以参考 [环境配置](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra07-环境配置.md)。
 
 ### 1.3.3 执行行动循环
 

+ 1 - 1
docs/chapter3/第三章 大语言模型基础.md

@@ -797,7 +797,7 @@ model = AutoModelForCausalLM.from_pretrained(model_id).to(device)
 print("模型和分词器加载完成!")
 ```
 
-我们来创建一个对话提示,Qwen1.5-Chat 模型遵循特定的对话模板。然后,可以使用上一步加载的 `tokenizer` 将文本提示转换为模型能够理解的数字 ID(即 Token ID)。
+我们来创建一个对话提示,Qwen1.5-Chat 模型遵循特定的对话模板。然后,可以使用上一步加载的 `tokenizer` 将文本提示转换为模型能够理解的数字 ID(即 Token ID)。
 
 ```Python
 # 准备对话输入

+ 10 - 6
docs/chapter4/Chapter4-Building-Classic-Agent-Paradigms.md

@@ -32,7 +32,7 @@ To make our code more universal, we will uniformly configure model service-relat
 
 1. In your project root directory, create a file named `.env`.
 2. In this file, add the following content. You can point it to OpenAI's official service or any local/third-party service compatible with the OpenAI interface according to your needs.
-3. If you really don't know how to obtain it, you can refer to Section [1.2 API 设置](https://datawhalechina.github.io/handy-multi-agent/#/./chapter1/1-Basic-Configuration?id=_12-api-settings) in another Datawhale tutorial.
+3. If you really don't know how to obtain it, you can refer to [Environment Configuration](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra07-环境配置.md).
 
 ```bash
 # .env file
@@ -434,16 +434,20 @@ The LLM returns plain text, and we need to precisely extract `Thought` and `Acti
 ```python
 # (These methods are part of the ReActAgent class)
     def _parse_output(self, text: str):
-        """Parse LLM output to extract Thought and Action."""
-        thought_match = re.search(r"Thought: (.*)", text)
-        action_match = re.search(r"Action: (.*)", text)
+        """Parse LLM output to extract Thought and Action.
+        """
+        # Thought: match until Action: or end of text
+        thought_match = re.search(r"Thought:\s*(.*?)(?=\nAction:|$)", text, re.DOTALL)
+        # Action: match until end of text
+        action_match = re.search(r"Action:\s*(.*?)$", text, re.DOTALL)
         thought = thought_match.group(1).strip() if thought_match else None
         action = action_match.group(1).strip() if action_match else None
         return thought, action
 
     def _parse_action(self, action_text: str):
-        """Parse Action string to extract tool name and input."""
-        match = re.match(r"(\w+)\[(.*)\]", action_text)
+        """Parse Action string to extract tool name and input.
+        """
+        match = re.match(r"(\w+)\[(.*)\]", action_text, re.DOTALL)
         if match:
             return match.group(1), match.group(2)
         return None, None

+ 11 - 7
docs/chapter4/第四章 智能体经典范式构建.md

@@ -32,7 +32,7 @@ pip install openai python-dotenv
 
 1. 在你的项目根目录下,创建一个名为 `.env` 的文件。
 2. 在该文件中,添加以下内容。你可以根据自己的需要,将其指向 OpenAI 官方服务,或任何兼容 OpenAI 接口的本地/第三方服务。
-3. 如果实在不知道如何获取,可以参考Datawhale另一本教程的[1.2 API 设置](https://datawhalechina.github.io/handy-multi-agent/#/./chapter1/1-Basic-Configuration?id=_12-api-settings)
+3. 如果实在不知道如何获取,可以参考 [环境配置](https://github.com/datawhalechina/hello-agents/blob/main/Extra-Chapter/Extra07-环境配置.md)
 
 ```bash
 # .env file
@@ -174,7 +174,7 @@ $$o_t = T(a_t)$$
 
 如果说大语言模型是智能体的大脑,那么<strong>工具 (Tools)</strong> 就是其与外部世界交互的“手和脚”。为了让ReAct范式能够真正解决我们设定的问题,智能体需要具备调用外部工具的能力。
 
-针对本节设定的目标——回答关于“华为最新手机”的问题,我们需要为智能体提供一个网页搜索工具。在这里我们选用 <strong>SerpApi</strong>,它通过API提供结构化的Google搜索结果,能直接返回“答案摘要框”或精确的知识图谱信息
+针对本节设定的目标——回答关于“华为最新手机”的问题,我们需要为智能体提供一个网页搜索工具。在这里我们选用 <strong>SerpApi</strong>,它通过API提供结构化的Google搜索结果,能直接返回“答案摘要框”或精确的知识图谱信息
 
 首先,需要安装该库:
 
@@ -434,16 +434,20 @@ LLM 返回的是纯文本,我们需要从中精确地提取出`Thought`和`Act
 ```python
 # (这些方法是 ReActAgent 类的一部分)
     def _parse_output(self, text: str):
-        """解析LLM的输出,提取Thought和Action。"""
-        thought_match = re.search(r"Thought: (.*)", text)
-        action_match = re.search(r"Action: (.*)", text)
+        """解析LLM的输出,提取Thought和Action。
+        """
+        # Thought: 匹配到 Action: 或文本末尾
+        thought_match = re.search(r"Thought:\s*(.*?)(?=\nAction:|$)", text, re.DOTALL)
+        # Action: 匹配到文本末尾
+        action_match = re.search(r"Action:\s*(.*?)$", text, re.DOTALL)
         thought = thought_match.group(1).strip() if thought_match else None
         action = action_match.group(1).strip() if action_match else None
         return thought, action
 
     def _parse_action(self, action_text: str):
-        """解析Action字符串,提取工具名称和输入。"""
-        match = re.match(r"(\w+)\[(.*)\]", action_text)
+        """解析Action字符串,提取工具名称和输入。
+        """
+        match = re.match(r"(\w+)\[(.*)\]", action_text, re.DOTALL)
         if match:
             return match.group(1), match.group(2)
         return None, None

+ 68 - 0
docs/chapter7/Chapter7-Building-Your-Agent-Framework.md

@@ -1279,6 +1279,74 @@ As shown in Table 7.2, through this framework refactoring, we not only maintaine
   <img src="https://raw.githubusercontent.com/datawhalechina/Hello-Agents/main/docs/images/7-figures/table-02.png" alt="" width="90%"/>
 </div>
 
+### 7.4.5 FunctionCallAgent
+
+FunctionCallAgent is an Agent introduced in hello-agents after version 0.2.8, based on OpenAI's native function calling mechanism. It demonstrates how to build an Agent using OpenAI's function calling capabilities.
+It supports the following features:
+
+- _build_tool_schemas: Constructs OpenAI function calling schema through tool descriptions
+- _extract_message_content: Extracts text content from OpenAI responses
+- _parse_function_call_arguments: Parses JSON string parameters returned by the model
+- _convert_parameter_types: Converts parameter types
+
+These features enable native OpenAI Function Calling capabilities, providing stronger robustness compared to prompt-constrained approaches.
+
+```python
+def _invoke_with_tools(self, messages: list[dict[str, Any]], tools: list[dict[str, Any]], tool_choice: Union[str, dict], **kwargs):
+        """Invoke underlying OpenAI client to execute function calls"""
+        client = getattr(self.llm, "_client", None)
+        if client is None:
+            raise RuntimeError("HelloAgentsLLM client not properly initialized, cannot execute function calls.")
+
+        client_kwargs = dict(kwargs)
+        client_kwargs.setdefault("temperature", self.llm.temperature)
+        if self.llm.max_tokens is not None:
+            client_kwargs.setdefault("max_tokens", self.llm.max_tokens)
+
+        return client.chat.completions.create(
+            model=self.llm.model,
+            messages=messages,
+            tools=tools,
+            tool_choice=tool_choice,
+            **client_kwargs,
+        )
+
+# Internal logic wraps OpenAI native function calling
+# OpenAI native function calling example
+from openai import OpenAI
+client = OpenAI()
+
+tools = [
+  {
+    "type": "function",
+    "function": {
+      "name": "get_current_weather",
+      "description": "Get the current weather in a given location",
+      "parameters": {
+        "type": "object",
+        "properties": {
+          "location": {
+            "type": "string",
+            "description": "The city and state, e.g. San Francisco, CA",
+          },
+          "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
+        },
+        "required": ["location"],
+      },
+    }
+  }
+]
+messages = [{"role": "user", "content": "What's the weather like in Boston today?"}]
+completion = client.chat.completions.create(
+  model="gpt-5",
+  messages=messages,
+  tools=tools,
+  tool_choice="auto"
+)
+
+print(completion)
+```
+
 ## 7.5 Tool System
 
 The content of this section will deeply explore the design and implementation of the tool system based on the Agent infrastructure built earlier. We will start from infrastructure construction and gradually delve into custom development design. The learning objectives of this section revolve around the following three core aspects:

+ 5 - 4
docs/chapter7/第七章 构建你的Agent框架.md

@@ -1283,10 +1283,11 @@ print(f"数学专用Agent结果: {math_result}")
 
 FunctionCallAgent是hello-agents在0.2.8之后引入的Agent,它基于OpenAI原生函数调用机制的Agent,展示了如何使用OpenAI的函数调用机制来构建Agent。
 它支持以下功能:
-_build_tool_schemas:通过工具的description构建OpenAI的function calling schema
-_extract_message_content:从OpenAI的响应中提取文本
-_parse_function_call_arguments:解析模型返回的JSON字符串参数
-_convert_parameter_types:转换参数类型
+
+- _build_tool_schemas:通过工具的description构建OpenAI的function calling schema
+- _extract_message_content:从OpenAI的响应中提取文本
+- _parse_function_call_arguments:解析模型返回的JSON字符串参数
+- _convert_parameter_types:转换参数类型
 
 这些功能可以使其具备原生的OpenAI Function Calling的能力,对比使用prompt约束的方式,具备更强的鲁棒性。
 ```python

BIN
docs/images/star-history-2026113.png


BIN
docs/images/star-history-2026210.png


Некоторые файлы не были показаны из-за большого количества измененных файлов