AI Agent 学习笔记(七):综合实战,从零写一个学习笔记助手

AI Agent 学习笔记(七):综合实战,从零写一个学习笔记助手

终章来了!我们把前六篇的知识全部串起来,做一个完整的 Agent:学习笔记助手——能记住你、能查你的笔记、能算数、能保存新笔记,还能按“技能规范”写博客。

一. 项目目标

这个 Agent 要具备:

能力 用的知识 说明
记住用户偏好 Memory(第二篇) SQLite 存长期记忆,滑动窗口管短期记忆
基于笔记回答 RAG(第四篇) Chroma 检索本地笔记
计算与联网 Tool(第三篇) Function Calling:calculator、save_note、web_search
按规范写作 Skill(第五篇) 加载 SKILL.md 注入写作规范
对外暴露能力 MCP(第六篇) 可选:把“查笔记”做成 MCP Server
整体控制 Prompt(第一篇) 系统提示词编排一切

二. 架构图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌──────────────────────────────────────────────────────────┐
│ main.py(宿主程序) │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ 短期记忆 │ │ 工具执行器 │ │ 技能加载器 │ │
│ │ 滑动窗口 │ │ Function │ │ SKILL.md 注入│ │
│ └────┬─────┘ │ Calling 循环 │ └───────┬───────┘ │
│ │ └──────┬───────┘ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ LLM(大模型 API) │ │
│ └─────────────────────────────────────────────────┘ │
│ ▲ ▲ ▲ │
│ │ │ │ │
│ ┌────┴─────┐ ┌──────┴──────┐ ┌────────┴───────┐ │
│ │ SQLite │ │ Chroma 向量 │ │ MCP Server │ │
│ │ 长期记忆 │ │ 库(RAG) │ │ (可选对外接口) │ │
│ └──────────┘ └─────────────┘ └────────────────┘ │
└──────────────────────────────────────────────────────────┘

三. 项目结构

1
2
3
4
5
6
7
8
note-agent/
├── main.py # 主程序:Agent 循环
├── skills/
│ └── blog-writing/
│ └── SKILL.md # 博客写作技能
├── notes/ # 你的学习笔记(Markdown)
├── memory.db # SQLite(自动创建)
└── rag_db/ # Chroma 向量库(自动创建)

四. 完整代码

4.1 技能文件 skills/blog-writing/SKILL.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
---
name: blog-writing
description: 撰写技术博客文章。当用户要求写博客、写教程、整理学习笔记成文章时使用。
---

# 博客写作技能

## 写作流程
1. 先确认主题、读者水平、篇幅;
2. 结构:标题 → 引言 → 分节(概念/示例/对比) → 小结与作业;
3. 代码示例完整可运行,标注语言;
4. 首次出现的术语给通俗解释。

## 写作规范
- 简体中文,面向初学者;
- 多用表格对比,少用空洞形容;
- 每个代码块写明用途;
- 结尾固定给出“小结与作业”。

4.2 主程序 main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
"""学习笔记助手:整合 Prompt + Memory + Tool + RAG + Skill (+ 可选 MCP)"""
import json
import os
import re
import sqlite3
import datetime
from openai import OpenAI

client = OpenAI() # 环境变量 OPENAI_API_KEY

# ════════════════════════════════════════════
# 1. 短期记忆:滑动窗口(第二篇)
# ════════════════════════════════════════════
class ShortMemory:
def __init__(self, max_messages: int = 12):
self.history = []
self.max_messages = max_messages

def add(self, role: str, content: str):
self.history.append({"role": role, "content": content})
self.history = self.history[-self.max_messages:]

# ════════════════════════════════════════════
# 2. 长期记忆:SQLite(第二篇)
# ════════════════════════════════════════════
class LongMemory:
def __init__(self, db_path: str = "memory.db"):
self.conn = sqlite3.connect(db_path)
self.conn.execute("""
CREATE TABLE IF NOT EXISTS facts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
kind TEXT DEFAULT "fact",
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
""")
self.conn.commit()

def add(self, content: str, kind: str = "fact"):
self.conn.execute("INSERT INTO facts (content, kind) VALUES (?, ?)", (content, kind))
self.conn.commit()

def all(self) -> list:
cur = self.conn.execute("SELECT content, kind FROM facts ORDER BY id DESC LIMIT 20")
return [f"[{kind}] {content}" for content, kind in cur.fetchall()]

# ════════════════════════════════════════════
# 3. RAG:Chroma 检索笔记(第四篇)
# ════════════════════════════════════════════
import chromadb

class NoteRAG:
def __init__(self, notes_dir: str = "notes"):
self.notes_dir = notes_dir
self.client = chromadb.PersistentClient(path="./rag_db")
self.collection = self.client.get_or_create_collection("notes")

def index_notes(self):
"""把 notes/ 下的 Markdown 全部切块入库(资料更新后调用)"""
chunks, ids, metas = [], [], []
for idx, fname in enumerate(os.listdir(self.notes_dir)):
if not fname.endswith(".md"):
continue
path = os.path.join(self.notes_dir, fname)
text = open(path, encoding="utf-8").read()
# 简单按 500 字切块
for j in range(0, len(text), 500):
chunks.append(text[j:j + 500])
ids.append(f"{fname}-{j}")
metas.append({"source": fname})
if chunks:
self.collection.upsert(ids=ids, documents=chunks, metadatas=metas)
print(f"已索引 {len(chunks)} 个文本块")

def search(self, query: str, top_k: int = 3) -> str:
results = self.collection.query(query_texts=[query], n_results=top_k)
docs = results["documents"][0] if results["documents"] else []
return "\n\n".join(f"[资料{i + 1}] {d}" for i, d in enumerate(docs))

# ════════════════════════════════════════════
# 4. 工具层(第三篇)
# ════════════════════════════════════════════
def calculator(expr: str) -> str:
allowed = set("0123456789+-*/(). ")
if not set(expr).issubset(allowed):
return "错误:表达式包含非法字符"
try:
return str(eval(expr, {"__builtins__": {}}, {}))
except Exception as e:
return f"计算失败:{e}"

def save_note(title: str, content: str) -> str:
"""把学习笔记保存到 notes/ 目录,并加入知识库"""
path = f"notes/{title}.md"
with open(path, "w", encoding="utf-8") as f:
f.write(f"# {title}\n\n{content}")
rag.index_notes() # 新笔记立即进知识库
return f"已保存到 {path}"

def web_search(query: str) -> str:
"""演示用搜索(真实项目请替换为搜索 API 或爬虫)"""
return f"【模拟搜索结果】关于“{query}”:暂无实时数据,建议访问搜索引擎确认。"

def get_time() -> str:
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

TOOLS = [
{"type": "function", "function": {"name": "calculator", "description": "当用户需要数学计算时使用", "parameters": {"type": "object", "properties": {"expr": {"type": "string"}}, "required": ["expr"]}}},
{"type": "function", "function": {"name": "save_note", "description": "当用户要保存学习笔记时使用", "parameters": {"type": "object", "properties": {"title": {"type": "string"}, "content": {"type": "string"}}, "required": ["title", "content"]}}},
{"type": "function", "function": {"name": "web_search", "description": "当用户需要实时信息时使用", "parameters": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}}},
{"type": "function", "function": {"name": "get_time", "description": "查询当前日期时间", "parameters": {"type": "object", "properties": {}}}},
]

TOOL_FUNCS = {
"calculator": calculator,
"save_note": save_note,
"web_search": web_search,
"get_time": get_time,
}

# ════════════════════════════════════════════
# 5. 技能加载器(第五篇)
# ════════════════════════════════════════════
def load_skill(skill_name: str) -> str:
path = f"skills/{skill_name}/SKILL.md"
if os.path.isfile(path):
return open(path, encoding="utf-8").read()
return ""

# ════════════════════════════════════════════
# 6. 全局状态
# ════════════════════════════════════════════
short_mem = ShortMemory()
long_mem = LongMemory()
rag = NoteRAG()

# ════════════════════════════════════════════
# 7. 系统提示词(第一篇:编排一切)
# ════════════════════════════════════════════
def build_system_prompt(task: str) -> str:
skill_text = load_skill("blog-writing")
facts = ";".join(long_mem.all()) if long_mem.all() else "暂无"
return f"""你是一个学习笔记助手。

# 用户长期记忆
已知用户信息:{facts}

# 工作规则
1. 需要实时信息或计算时,调用对应工具;
2. 用户问笔记内容时,先检索知识库再回答;
3. 用户要求写博客/整理笔记成文章时,严格按技能规范执行;
4. 回答要简洁,重要信息用列表;
5. 新学到的用户偏好,回答末尾用 <remember>标签包起来,方便程序抽取。

# 写作技能(仅在写博客时生效)
{skill_text if '写' in task else '(本次任务不涉及写作技能)'}"""

def extract_remember(text: str):
"""从回复里抽取 <remember> 标签并存入长期记忆"""
for m in re.findall(r"<remember>(.*?)</remember>", text):
long_mem.add(m.strip(), "preference")

# ════════════════════════════════════════════
# 8. Agent 主循环
# ════════════════════════════════════════════
def run(user_input: str):
# 先检索知识库,把相关资料放进本轮上下文
rag_context = rag.search(user_input)
short_mem.add("user", f"[知识库检索]\n{rag_context}\n\n用户问题:{user_input}")

messages = [
{"role": "system", "content": build_system_prompt(user_input)},
*short_mem.history,
]

for _ in range(8):
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=TOOLS,
)
msg = resp.choices[0].message

if not msg.tool_calls:
extract_remember(msg.content or "")
short_mem.add("assistant", msg.content)
return msg.content

messages.append(msg)
for call in msg.tool_calls:
try:
args = json.loads(call.function.arguments or "{}")
result = TOOL_FUNCS[call.function.name](**args)
except Exception as e:
result = f"工具异常:{e}"
print(f" [工具] {call.function.name}{result[:60]}")
messages.append({"role": "tool", "tool_call_id": call.id, "content": str(result)})
return "已达最大循环次数,请简化问题后重试。"

# ════════════════════════════════════════════
# 9. 入口
# ════════════════════════════════════════════
if __name__ == "__main__":
rag.index_notes()
print("学习笔记助手已启动,输入 exit 退出。")
while True:
q = input("你:")
if q.strip().lower() == "exit":
break
print("助手:", run(q))

五. 运行与测试

1
2
3
4
5
6
7
8
# 1. 安装依赖
pip install openai chromadb

# 2. 在 notes/ 目录放几篇自己的 Markdown 笔记
mkdir -p notes skills/blog-writing

# 3. 运行
python main.py

建议按顺序测试这 5 个场景:

测试 输入 期望
记忆 “我喜欢简洁的回答” → 下轮问“我喜欢什么风格” 记住偏好
工具 “帮我算 2 的 10 次方” 调用 calculator
RAG “我的笔记里 RAG 的流程是什么” 基于笔记回答
工具+RAG “把 MCP 的学习笔记保存下来” save_note 后再次提问能查到
技能 “用博客格式帮我写一篇关于 Python 的笔记” 按 SKILL.md 结构输出

六. 加餐:把“查笔记”暴露成 MCP Server

如果你想把笔记能力给其他 Agent(比如 Claude Desktop)用,单独写一个 MCP Server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# notes_mcp_server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("notes-server")

@mcp.tool()
def search_notes(query: str, top_k: int = 3) -> str:
"""在个人学习笔记中检索相关内容"""
# 复用上一节 NoteRAG 的逻辑(简化写法)
import chromadb
client = chromadb.PersistentClient(path="./rag_db")
col = client.get_or_create_collection("notes")
res = col.query(query_texts=[query], n_results=top_k)
return "\n".join(res["documents"][0]) if res["documents"] else "未找到相关内容"

if __name__ == "__main__":
mcp.run()
1
2
python notes_mcp_server.py   # 默认 stdio 模式
# 然后在支持 MCP 的客户端里配置 command: python, args: [notes_mcp_server.py]

七. 项目可以怎么继续升级

  1. 多轮 RAG:检索不到时,先调用 web_search 再回答;
  2. 记忆进化:定期让 LLM 总结旧记忆、合并重复偏好;
  3. 技能扩展:再加 code-review、summary 等 SKILL.md,让加载器自动发现;
  4. 多 MCP Server:挂上 GitHub、数据库 Server,Agent 直接操作外部系统;
  5. 可观测性:记录每次工具调用、Token 消耗、检索来源,方便排查问题;
  6. 异步与并发:多个工具并行执行(第三篇提过),加快响应。

八. 整个系列回顾

主题 一句话总结
入门 Agent = LLM + 规划 + 记忆 + 工具 + 行动循环
Prompt 角色 + 具体指令 + 示例 + 格式,让模型听话
Memory 短期管上下文,长期存外部,向量做语义记忆
Tool 模型点单、代码执行,Function Calling 循环
RAG 分块 → 向量化 → 检索 → 看着资料回答
Skill 把提示词+流程+资源打包成 SKILL.md
MCP 统一协议,让工具即插即用
实战 以上全部整合成一个可运行的 Agent

九. 结语

从第一篇到现在,你已经走完了“概念 → 单点能力 → 综合项目”的完整路径。接下来推荐:

  1. 读一遍 ReAct 论文和 Toolformer,理解底层原理;
  2. 上手一个框架(LangGraph / LlamaIndex / Dify),看看工程化怎么处理边界情况;
  3. 给自己定一个真实小项目(比如“追番情报助手”“作业提醒 Agent”),边做边学;
  4. 关注 MCP 生态,它正在成为 Agent 连接世界的标准。

记住:Agent 的能力 = 模型的能力 × 提示词 × 工具 × 记忆 × 检索 × 工程兜底。祝你写出自己的第一个智能体!