AI项目实战(12)全能个人助理 Agent

《AI Agent 实战》系列 · 全能个人助理 Agent

Posted by Ryan on 2026-07-04
Estimated Reading Time 48 Minutes
Words 11.4k In Total
Viewed Times

主讲能力:MCP 思维、短长期记忆、多工具调度、个人偏好沉淀
业务场景:一个统一的私人助理,帮用户处理邮件、日历、文件、偏好记忆等日常事务。
对应代码backend/projects/p09_personal_assistant/


11.1 老板的诉求

先讲个真事。我认识一位做投资的老板,每天邮件上百封、会议排到下班、文件散在三四个盘里,事务繁杂到必须配助理。他前后换了三位人类助理,离职原因惊人地一致——记不住他的习惯。第一位帮他订机票,永远忘记他要靠窗;第二位帮他点工作餐,三次里两次带了含花生的菜,而他花生过敏;第三位倒是勤快,可每次都要重新问一遍"您开会喜欢哪个会议室"。他跟朋友吐槽:“我要的不是会做事的人,是知道我是什么人的人。”

这句话,恰恰戳中了 Agent 落地的一个核心命题。

个人助理是 Agent 最自然的应用场景之一。人类助理真正值钱的地方,从来不是会做某一个动作——发邮件、订会议谁都会——而是知道你是谁、记得你的习惯、理解上下文、把多个系统串成一件事。会发邮件的叫工具,记得你对花生过敏的才叫助理。

本项目要造的,就是这么一个"有记忆的私人助理"雏形:

  • 发送邮件
  • 安排会议
  • 搜索文件
  • 记住偏好
  • 根据长期记忆个性化回复

功能需求

功能 描述
邮件 模拟发送邮件,生产可替换 SMTP/MCP 邮箱 Server
日历 模拟安排会议,生产可接 Google Calendar/Exchange
文件 搜索文件,生产可接本地文件 MCP Server
偏好记忆 记住用户偏好,跨会话复用
记忆检索 根据当前问题检索相关长期记忆

11.2 画个样子:它该长啥样

把上面那张"助理画像"翻译成架构,就是下面这张图:用户一句话进来,Agent 调度邮件、日历、文件、记忆四类工具,记忆沉淀到 SQLite,最后吐出一句"懂你"的个性化回复。

%%{init: {'theme':'base','flowchart':{'useMaxWidth':true,'htmlLabels':true}}}%%
graph TD
    U["用户请求"] --> A["个人助理 Agent"]
    A --> Mail["邮件工具"]
    A --> Cal["日历工具"]
    A --> File["文件搜索"]
    A --> Mem["记忆系统"]
    Mem --> DB[("SQLite 长期记忆")]
    A --> Reply["个性化回复"]

    classDef a fill:#ede7f6,stroke:#5e35b1,stroke-width:2px,color:#311b92
    classDef t fill:#e0f7fa,stroke:#00acc1,stroke-width:2px,color:#006064
    classDef d fill:#e8f5e9,stroke:#43a047,stroke-width:2px,color:#1b5e20
    class A a
    class Mail,Cal,File,Mem t
    class DB,Reply d

11.3 拆开看:怎么造出来

11.3.1 记忆系统

回到那位老板。前两位助理的问题不是不努力,是"没记住";可如果反过来,助理把老板说过的每一句话都背下来、逢人就复述,那也不行——那叫八卦,不叫贴心。记忆这件事,少了不贴心,多了不安全。

PersonalMemory 就是来拿捏这个分寸的,它同时提供两类记忆:

  • 短期记忆:进程内字典,适合当前会话——相当于助理"手边的便签",会话一结束就撕掉。
  • 长期记忆:SQLite 持久化,适合跨会话偏好——相当于助理的"小本本",关机再开机还在。
1
2
mem.remember_preference("preferred_room", "A101", description="开会偏爱用的房间")
mem.recall_preference("preferred_room")

说明:本书早期草稿曾用 remember_long/recall_long 这样的通用命名来示意长期记忆,但落地代码里长期记忆的入口是围绕"偏好"设计的 remember_preference/recall_preference,另有 search_preferences 做模糊检索;对话和任务则各自有独立表,不挤在同一个 API 里。下文的完整代码会原样呈现这一实现。

为什么不用纯上下文?因为上下文会随会话结束而丢失,且 token 成本随长度线性上涨——把老板三年的对话全塞进上下文,既贵又慢,还会被模型遗忘。生产系统必须把稳定偏好沉淀到外部存储。

💡 顿悟时刻:长期记忆的本质,其实就是 RAG。 把用户的偏好、决策、重要事实写进外部库(这里是 SQLite),回答前先用关键词或向量检索出"跟当前问题相关的那几条",拼进提示词喂给模型——这不就是 Retrieve-Augmented Generation 吗?只不过这里的"知识库"不是公司文档,而是用户自己的小本本。本章用 LIKE 做的是最朴素的检索器,生产环境换成向量召回,就是一套完整的个人 RAG。

⚠️ 避坑:别把所有对话都倒进记忆。 有人会想,既然能存,那干脆把每句对话都存成长期记忆,岂不更"懂我"?恰恰相反。一是成本——全量历史塞进上下文,token 立刻爆炸;二是噪声——大部分对话是寒暄和即兴闲聊,存进去只会稀释信号,检索时把真正重要的偏好淹没掉;三是时效——旧偏好会和新偏好打架(“我喜欢靠窗” vs 后来的"我现在prefer走道");四是隐私——存得越多,泄露时的杀伤面越大。正确做法是:对话归对话(落日志表),偏好归偏好(精选沉淀)。本项目正是这么分的——conversations 表只做可追溯的流水,preferences 表才进检索,各司其职。

11.3.2 MCP 思维

助理要替你办事,就得能"伸手"去够外面的世界——发邮件得够到邮箱,订会议得够到日历,找文件得够到磁盘。问题是,这些系统的协议千差万别,要是每接一个就硬编码一套,助理的代码很快就成一团乱麻。

本章代码先用本地函数把邮件/日历/文件这些能力"演"出来,让你聚焦在助理本身的记忆与调度上。但请心里始终挂着一件事:生产环境里,这些函数的每一个,都应该被替换成一个 MCP Server:

当前工具 生产替代
send_email 邮箱 MCP Server
schedule_meeting 日历 MCP Server
search_files 文件系统 MCP Server

本章真正想讲的,不是某一个具体的 SaaS API 怎么调,而是用一套标准接口把私人助理接入外部世界这个思维方式。工具今天接 SMTP、明天接 SendGrid、后天换成 MCP 邮箱 Server——对 Agent 来说,调用的形状不变,助理的"脑子"不用动。这就是 MCP 思维的价值:让助理的能力可插拔,而不是被某个厂商焊死。


11.4 动手写:三层架构完整代码

设计讲完,开始动手。本节给出全能个人助理 Agent 的完整代码实现。项目采用分层架构:模型层、提示词层、工具层、记忆层、服务层、项目层各司其职,配合入口文件完成注册与对外导出。这么分不是为了"显得专业",而是因为个人助理天生要频繁替换工具(接 MCP)、频繁演进记忆策略——分层之后,换工具不动服务、调记忆不动提示词,整体清晰、好维护、好扩展。

11.4.1 三层架构完整代码

下表列出各层的文件与职责,先有个全局地图,再逐层展开:

层次 文件 职责
模型层 models.py 助理能力、用户偏好、日历/邮件/文件模型
提示词层 prompts.py 助理系统提示词、邮件/日历/偏好模板
工具层 tools.py 邮件/日历/文件/记忆/任务工具
记忆层 memory.py 短期/长期记忆系统(SQLite)
服务层 service.py 上下文管理、个性化服务
项目层 project.py 项目注册、对外接口
入口 init.py 注册与 re-export

下面按层次依次给出每个文件的完整代码。代码与仓库 backend/projects/p09_personal_assistant/ 中的实现保持一致。

models.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
"""项目九:数据模型层。

定义全能个人助理 Agent 的领域模型和数据结构。
"""
from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any


class AssistantCapability(str, Enum):
"""助理能力枚举。"""
EMAIL = "email" # 邮件管理
CALENDAR = "calendar" # 日程管理
FILES = "files" # 文件检索
NOTES = "notes" # 笔记管理
TASKS = "tasks" # 任务管理
CONTACTS = "contacts" # 联系人管理
REMINDERS = "reminders" # 提醒
PREFERENCES = "preferences" # 用户偏好


class MessageType(str, Enum):
"""消息类型枚举。"""
USER = "user"
ASSISTANT = "assistant"
SYSTEM = "system"


@dataclass
class UserPreference:
"""用户偏好设置。"""
key: str
value: str
category: str = "general"
description: str = ""
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())

def to_dict(self) -> dict[str, str]:
"""转换为字典。"""
return {
"key": self.key,
"value": self.value,
"category": self.category,
"description": self.description,
"updated_at": self.updated_at,
}


@dataclass
class CalendarEvent:
"""日历事件。"""
title: str
date: str
time: str = ""
attendees: list[str] = field(default_factory=list)
location: str = ""
description: str = ""
reminder_minutes: int = 30

def to_summary(self) -> str:
"""生成摘要。"""
attendees_str = ", ".join(self.attendees) if self.attendees else "无参会人"
return f"📅 {self.title} - {self.date} {self.time} | 参会人: {attendees_str}"


@dataclass
class EmailMessage:
"""邮件消息。"""
to: str
subject: str
body: str
cc: list[str] = field(default_factory=list)
priority: str = "normal"

def to_summary(self) -> str:
"""生成摘要。"""
return f"📧 邮件: {self.subject} | 收件人: {self.to}"


@dataclass
class FileSearchResult:
"""文件搜索结果。"""
filename: str
path: str
file_type: str
size_bytes: int = 0
last_modified: str = ""

def to_summary(self) -> str:
"""生成摘要。"""
size_mb = self.size_bytes / (1024 * 1024) if self.size_bytes > 0 else 0
return f"📄 {self.filename} ({self.file_type}, {size_mb:.1f} MB) - {self.path}"


@dataclass
class ConversationMessage:
"""对话消息。"""
role: MessageType
content: str
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
metadata: dict[str, Any] = field(default_factory=dict)


@dataclass
class AssistantContext:
"""助理上下文。

包含所有与用户交互相关的状态和数据。
"""
user_id: str
conversation_history: list[ConversationMessage] = field(default_factory=list)
pending_tasks: list[dict[str, Any]] = field(default_factory=list)
active_context: dict[str, Any] = field(default_factory=dict)

def add_message(self, role: MessageType, content: str, **metadata: Any) -> None:
"""添加消息到对话历史。

Args:
role: 消息角色
content: 消息内容
metadata: 额外元数据
"""
self.conversation_history.append(ConversationMessage(
role=role,
content=content,
metadata=metadata,
))
# 保留最近 50 条消息
if len(self.conversation_history) > 50:
self.conversation_history = self.conversation_history[-50:]

def get_recent_messages(self, limit: int = 10) -> list[ConversationMessage]:
"""获取最近的消息。

Args:
limit: 返回数量限制

Returns:
消息列表
"""
return self.conversation_history[-limit:]

def set_context(self, key: str, value: Any) -> None:
"""设置上下文变量。

Args:
key: 变量名
value: 变量值
"""
self.active_context[key] = value

def get_context(self, key: str, default: Any = None) -> Any:
"""获取上下文变量。

Args:
key: 变量名
default: 默认值

Returns:
变量值或默认值
"""
return self.active_context.get(key, default)

prompts.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
"""项目九:Prompt 层。

定义全能个人助理 Agent 的系统提示词。
"""
from __future__ import annotations

# 主助理系统提示词
ASSISTANT_PROMPT = """你是用户的全能私人助理,负责高效管理用户的日常事务。

## 你的能力
1. **邮件管理**:发送邮件、起草回复、整理收件箱、设置邮件提醒
2. **日历管理**:安排会议、设置提醒、查询日程、检测时间冲突
3. **文件管理**:搜索本地文件、整理文档、汇总内容
4. **记忆系统**:记住用户偏好、学习用户习惯、跨会话记住重要信息
5. **任务管理**:创建待办、设置优先级、跟踪进度、提醒完成

## 工作原则
1. **主动获取上下文**:在回答前,先检查用户偏好和历史记忆
2. **简洁清晰**:回答简洁明了,重点突出,不啰嗦
3. **涉及费用/权限/安全时,明确确认**:对于涉及金钱、权限变更、敏感操作,必须二次确认
4. **用户隐私优先**:不泄露用户敏感信息,操作有日志可追溯
5. **超时提醒**:长时间运行的操作要给出进度反馈

## 记忆使用指南
- 遇到用户习惯相关问题时,先调用 search_memories 查找历史偏好
- 用户明确表达的偏好,调用 remember_preference 保存
- 重要决策和选择,保存到长期记忆供未来参考

## 回复风格
- 友好专业,不过于随意
- 使用适当的 emoji 增加可读性,但不过度使用
- 复杂问题分点回答
- 给出明确的行动建议,不要只说 "好的" 或 "明白了"

## 错误处理
- 工具调用失败时,友好说明并给出替代方案
- 不确定时,向用户确认,不要猜测
- 承认能力边界,不编造信息

现在,请用你的能力帮助用户。记得:主动、高效、贴心!"""


# 邮件处理提示词
EMAIL_PROMPT = """你是专业的邮件助理,擅长撰写清晰、专业的商务邮件。

## 邮件撰写原则
1. **主题明确**:收件人一眼就能知道邮件的核心内容
2. **开头问候恰当**:根据关系远近选择合适的称呼
3. **正文简洁**:每段不超过 3 行,重点用加粗或列表
4. **行动明确**:需要对方做什么、什么时候做,写清楚
5. **结尾礼貌**:合适的敬语和签名

## 邮件类型模板
### 会议邀请
> 主题:会议邀请 - [会议主题]
>
> Hi [姓名],
>
> 想邀请你参加关于 [主题] 的会议,时间:[时间],地点:[地点/链接]。
>
> 主要议题:
> - 议题 1
> - 议题 2
>
> 请在 [日期] 前确认是否能参加。
>
> 谢谢!
> [你的名字]

### 任务跟进
> 主题:跟进 - [任务名]
>
> Hi [姓名],
>
> 想了解一下 [任务] 的进展,目前有什么需要我协助的吗?
>
> 期待你的回复。
>
> 谢谢!

## 注意事项
- 避免使用过于口语化的表达
- 重要信息前置
- 检查语法和拼写错误

请根据用户需求撰写邮件。"""


# 日程管理提示词
CALENDAR_PROMPT = """你是专业的日程管理助理,擅长安排和优化用户的时间。

## 日程安排原则
1. **避开工作时间外**:除非用户明确要求,不要在非工作时间安排会议
2. **预留缓冲**:会议之间至少留 15 分钟缓冲
3. **检测冲突**:安排前先检查是否有时间冲突
4. **提醒设置**:重要会议提前 30 分钟提醒,普通会议提前 15 分钟
5. **参会人确认**:列出所有参会人,确保没有遗漏

## 时间建议
- 上午 10:00-11:30:适合重要会议,大家精力充沛
- 下午 14:00-16:00:适合讨论和协作
- 周五下午:不适合安排重要会议

## 冲突处理
- 如果检测到冲突,列出冲突的事件
- 给出 2-3 个替代时间建议
- 说明每个建议的优缺点

请帮助用户合理安排日程。"""


# 用户偏好学习提示词
PREFERENCE_PROMPT = """你是用户偏好学习助理,负责记住并应用用户的习惯。

## 需要记住的用户偏好类型
1. **沟通偏好**:喜欢邮件还是即时消息?回复速度要求?
2. **时间偏好**:工作时间?喜欢什么时候开会?
3. **格式偏好**:喜欢长篇还是短篇?喜欢列表还是段落?
4. **内容偏好**:对什么话题感兴趣?想避免什么话题?
5. **工具偏好**:喜欢用什么工具?不喜欢用什么工具?

## 学习时机
- 用户明确说 "我喜欢/我偏好/我希望..." 时
- 用户反复纠正同一种行为时
- 用户表达满意或不满意时

## 记忆质量要求
- **key**:简洁,用英文加下划线,如 meeting_preference
- **value**:具体明确,不要模糊
- **description**:一句话说明这个偏好的含义
- **category**:分类,如 communication/time/format/content/tool

## 应用记忆
回答用户问题时,如果有相关记忆,引用记忆内容,并说明 "根据你的偏好..."

请记住并应用用户的偏好,提供更加个性化的服务。"""


# 任务管理提示词
TASK_PROMPT = """你是任务管理助理,帮助用户高效管理待办事项。

## 任务管理原则
1. **SMART 原则**:任务应该具体(Specific)、可衡量(Measurable)、可达成(Attainable)、相关(Relevant)、有时限(Time-bound)
2. **优先级**:按重要紧急程度划分:重要紧急 > 重要不紧急 > 紧急不重要 > 不紧急不重要
3. **拆解**:大任务拆成小步骤,降低执行门槛
4. **提醒**:设置合适的提醒时间
5. **进度跟踪**:定期回顾和更新状态

## 任务模板

任务:[任务名称]
优先级:[P0/P1/P2/P3]
截止日期:[YYYY-MM-DD]
子任务:

  1. [子任务 1]
  2. [子任务 2]
    提醒:[提前 X 天/小时]
1
2
3
4
5
6
7
8

## 优先级定义
- **P0**:今天必须完成,不做会出问题
- **P1**:本周必须完成,重要
- **P2**:本月内完成即可
- **P3**:有空再做,不着急

帮助用户把想法变成可执行的任务,提高效率和成就感。"""

tools.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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
"""项目九:工具层。

定义全能个人助理 Agent 的所有工具函数。
"""
from __future__ import annotations

from typing import Any

from langchain.tools import tool

from core.logging_conf import get_logger

from .memory import get_memory
from .models import CalendarEvent, EmailMessage, FileSearchResult

logger = get_logger("p09.assistant.tools")


# ========== 邮件工具 ==========
@tool
def send_email(to: str, subject: str, body: str, cc: str = "") -> str:
"""发送邮件(模拟实现,生产环境接入真实 SMTP 服务)。

Args:
to: 收件人邮箱
subject: 邮件主题
body: 邮件正文
cc: 抄送邮箱,多个用逗号分隔

Returns:
发送结果
"""
logger.info("发送邮件: to=%s subject=%s", to, subject)

email = EmailMessage(
to=to,
subject=subject,
body=body,
cc=cc.split(",") if cc else [],
)

# 生产环境:这里调用真实的邮件服务 API
# 例如:smtplib、SendGrid、Mailgun 等

return f"✅ {email.to_summary()}\n\n邮件已发送!"


@tool
def draft_email(recipient: str, purpose: str, key_points: str) -> str:
"""帮助用户起草邮件模板。

Args:
recipient: 收件人(如"客户"、"团队成员")
purpose: 邮件目的(如"邀请开会"、"跟进项目")
key_points: 要点,用逗号分隔

Returns:
邮件草稿
"""
logger.info("起草邮件: 收件人=%s 目的=%s", recipient, purpose)

points_list = [p.strip() for p in key_points.split(",") if p.strip()]
points_text = "\n".join([f"- {p}" for p in points_list])

draft = f"""
📧 邮件草稿
---
**主题**:{purpose}

Hi {recipient},

希望你一切都好。想和你沟通关于 {purpose} 的事情。

主要内容:
{points_text}

如果你有任何问题,随时联系我。

谢谢!
"""

return draft.strip()


# ========== 日历工具 ==========
@tool
def schedule_meeting(title: str, date: str, attendees: str, time: str = "10:00",
location: str = "") -> str:
"""安排会议(模拟日历操作,生产环境接入 Google Calendar / Exchange)。

Args:
title: 会议标题
date: 日期 (YYYY-MM-DD)
attendees: 参会人,多个用逗号分隔
time: 时间 (HH:MM)
location: 地点或会议链接

Returns:
会议安排结果
"""
logger.info("安排会议: %s on %s %s attendees=%s", title, date, time, attendees)

event = CalendarEvent(
title=title,
date=date,
time=time,
attendees=[a.strip() for a in attendees.split(",") if a.strip()],
location=location,
)

# 生产环境:这里调用真实的日历服务 API

return f"✅ {event.to_summary()}\n\n会议已安排!提前 30 分钟提醒。"


@tool
def check_availability(date: str) -> str:
"""检查某天的日程可用性(模拟)。

Args:
date: 日期 (YYYY-MM-DD)

Returns:
日程情况
"""
logger.info("检查日程: %s", date)

# 生产环境:这里查询真实的日历服务

return f"""
📅 {date} 日程情况:
---
- 09:00-10:00: 团队站会
- 11:00-12:00: 项目评审
- 14:00-16:00: [空闲] ✅ 推荐
- 16:00-17:00: [空闲] ✅ 推荐

建议:下午 2-4 点是最佳的会议时间。
""".strip()


# ========== 文件工具 ==========
@tool
def search_files(query: str) -> str:
"""搜索本地文件(模拟实现)。

Args:
query: 搜索关键词

Returns:
搜索结果
"""
logger.info("搜索文件: %s", query)

# 生产环境:这里调用系统搜索 API 或 Spotlight

results = [
FileSearchResult(
filename=f"{query}_report.pdf",
path=f"~/Documents/reports/{query}_report.pdf",
file_type="PDF 文档",
size_bytes=1024 * 1024 * 2,
),
FileSearchResult(
filename=f"{query}_draft.docx",
path=f"~/Documents/drafts/{query}_draft.docx",
file_type="Word 文档",
size_bytes=1024 * 500,
),
]

lines = [f"📄 搜索 '{query}' 找到 {len(results)} 个文件:", ""]
for r in results:
lines.append(r.to_summary())

return "\n".join(lines)


@tool
def summarize_file(filename: str) -> str:
"""汇总文件内容(模拟)。

Args:
filename: 文件名

Returns:
文件摘要
"""
logger.info("汇总文件: %s", filename)

# 生产环境:这里读取文件内容并调用 LLM 汇总

return f"""
📄 {filename} 摘要:
---
文件主要包含关于 {filename.replace('.pdf', '').replace('.docx', '')} 的内容。

核心要点:
- 要点 1:背景介绍
- 要点 2:主要数据
- 要点 3:结论建议

建议:阅读全文了解详细内容。
""".strip()


# ========== 记忆工具 ==========
@tool
def remember_preference(key: str, value: str, description: str = "") -> str:
"""保存用户偏好到长期记忆。

Args:
key: 偏好键(建议用英文下划线命名,如 meeting_preference)
value: 偏好值
description: 描述说明

Returns:
保存结果
"""
memory = get_memory()
memory.remember_preference(key, value, description=description)

return f"✅ 已记住:{key} = {value}"


@tool
def recall_preference(key: str) -> str:
"""获取用户偏好。

Args:
key: 偏好键

Returns:
偏好值或提示不存在
"""
memory = get_memory()
value = memory.recall_preference(key)

if value:
return f"📝 {key} = {value}"
return f"⚠️ 偏好 '{key}' 不存在"


@tool
def search_memories(query: str) -> str:
"""在长期记忆中搜索相关信息。

Args:
query: 搜索关键词

Returns:
匹配的记忆列表
"""
memory = get_memory()
preferences = memory.search_preferences(query)

if not preferences:
return f"🔍 未找到与 '{query}' 相关的记忆。"

lines = [f"🔍 找到 {len(preferences)} 条与 '{query}' 相关的记忆:", ""]
for p in preferences:
desc = f" - {p.description}" if p.description else ""
lines.append(f"- {p.key}: {p.value}{desc}")

return "\n".join(lines)


@tool
def list_all_preferences() -> str:
"""列出所有已保存的用户偏好。

Returns:
偏好列表
"""
memory = get_memory()
preferences = memory.get_all_preferences()

if not preferences:
return "📋 暂无保存的偏好。"

lines = [f"📋 已保存 {len(preferences)} 条偏好:", ""]
for p in preferences:
lines.append(f"- {p.key} = {p.value}")

return "\n".join(lines)


# ========== 任务工具 ==========
@tool
def create_task(title: str, priority: str = "P2", due_date: str = "") -> str:
"""创建待办任务。

Args:
title: 任务标题
priority: 优先级 (P0/P1/P2/P3)
due_date: 截止日期 (YYYY-MM-DD)

Returns:
创建结果
"""
logger.info("创建任务: %s 优先级=%s", title, priority)

# 生产环境:这里调用任务管理系统 API

due_text = f" 截止: {due_date}" if due_date else ""
return f"✅ 任务已创建:[{priority}] {title}{due_text}"


@tool
def list_tasks(status: str = "pending") -> str:
"""列出任务(模拟)。

Args:
status: 状态 (pending/completed/all)

Returns:
任务列表
"""
logger.info("列出任务: status=%s", status)

# 生产环境:这里查询任务管理系统

return f"""
📋 {status} 任务列表:
---
1. [P1] 完成项目方案 - 截止: 2024-01-20
2. [P2] 回复客户邮件 - 截止: 2024-01-18
3. [P3] 整理会议笔记

提示:使用 create_task 添加新任务。
""".strip()


# ========== 工具列表 ==========
def get_all_tools() -> list[Any]:
"""获取所有可用工具列表。

Returns:
工具对象列表
"""
return [
# 邮件工具
send_email,
draft_email,
# 日历工具
schedule_meeting,
check_availability,
# 文件工具
search_files,
summarize_file,
# 记忆工具
remember_preference,
recall_preference,
search_memories,
list_all_preferences,
# 任务工具
create_task,
list_tasks,
]

memory.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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
"""项目九:记忆层。

定义全能个人助理 Agent 的短期和长期记忆系统。
"""
from __future__ import annotations

import sqlite3
from datetime import datetime
from pathlib import Path
from typing import Any

from core.config import DATA_DIR
from core.logging_conf import get_logger

from .models import UserPreference

logger = get_logger("p09.assistant.memory")

DB_PATH = DATA_DIR / "personal_assistant.db"


class PersonalMemory:
"""个人记忆系统。

管理用户的短期记忆(会话内)和长期记忆(持久化):
- 短期记忆:当前会话的上下文、临时状态
- 长期记忆:用户偏好、历史行为、重要决策(SQLite 持久化)
"""

def __init__(self, db_path: Path | None = None, user_id: str = "default") -> None:
self._user_id = user_id
self._short_term: dict[str, Any] = {} # 短期记忆(仅当前会话)
self._db_path = db_path or DB_PATH
self._init_db()

def _init_db(self) -> None:
"""初始化数据库表结构。"""
conn = sqlite3.connect(str(self._db_path))
try:
# 用户偏好表
conn.execute("""
CREATE TABLE IF NOT EXISTS preferences (
user_id TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
category TEXT NOT NULL DEFAULT 'general',
description TEXT,
updated_at TEXT NOT NULL,
PRIMARY KEY (user_id, key)
)
""")

# 对话历史表
conn.execute("""
CREATE TABLE IF NOT EXISTS conversations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
timestamp TEXT NOT NULL,
metadata_json TEXT
)
""")

# 任务表
conn.execute("""
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
title TEXT NOT NULL,
priority TEXT NOT NULL DEFAULT 'P2',
status TEXT NOT NULL DEFAULT 'pending',
due_date TEXT,
created_at TEXT NOT NULL,
completed_at TEXT
)
""")

conn.commit()
logger.info("记忆系统初始化完成: %s", self._db_path)
finally:
conn.close()

# ========== 短期记忆 ==========
def remember_short(self, key: str, value: Any) -> None:
"""保存短期记忆(仅当前会话有效)。

Args:
key: 记忆键
value: 记忆值
"""
self._short_term[key] = value
logger.debug("保存短期记忆: %s", key)

def recall_short(self, key: str, default: Any = None) -> Any:
"""获取短期记忆。

Args:
key: 记忆键
default: 默认值

Returns:
记忆值或默认值
"""
return self._short_term.get(key, default)

def clear_short_term(self) -> None:
"""清空短期记忆。"""
self._short_term.clear()
logger.debug("短期记忆已清空")

# ========== 长期记忆(偏好) ==========
def remember_preference(self, key: str, value: str, category: str = "general",
description: str = "") -> None:
"""保存用户偏好到长期记忆。

Args:
key: 偏好键
value: 偏好值
category: 分类
description: 描述
"""
conn = sqlite3.connect(str(self._db_path))
try:
conn.execute(
"""
INSERT OR REPLACE INTO preferences
(user_id, key, value, category, description, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
self._user_id,
key,
value,
category,
description,
datetime.now().isoformat(),
),
)
conn.commit()
logger.info("保存偏好: %s = %s", key, value)
finally:
conn.close()

def recall_preference(self, key: str, default: str = "") -> str:
"""获取用户偏好。

Args:
key: 偏好键
default: 默认值

Returns:
偏好值或默认值
"""
conn = sqlite3.connect(str(self._db_path))
try:
row = conn.execute(
"SELECT value FROM preferences WHERE user_id = ? AND key = ?",
(self._user_id, key),
).fetchone()
return row[0] if row else default
finally:
conn.close()

def get_all_preferences(self) -> list[UserPreference]:
"""获取所有用户偏好。

Returns:
偏好列表
"""
conn = sqlite3.connect(str(self._db_path))
conn.row_factory = sqlite3.Row
try:
rows = conn.execute(
"""
SELECT key, value, category, description, updated_at
FROM preferences
WHERE user_id = ?
ORDER BY updated_at DESC
""",
(self._user_id,),
).fetchall()

return [
UserPreference(
key=row["key"],
value=row["value"],
category=row["category"],
description=row["description"] or "",
updated_at=row["updated_at"],
)
for row in rows
]
finally:
conn.close()

def search_preferences(self, keyword: str) -> list[UserPreference]:
"""搜索用户偏好。

Args:
keyword: 搜索关键词

Returns:
匹配的偏好列表
"""
conn = sqlite3.connect(str(self._db_path))
conn.row_factory = sqlite3.Row
try:
rows = conn.execute(
"""
SELECT key, value, category, description, updated_at
FROM preferences
WHERE user_id = ?
AND (key LIKE ? OR value LIKE ? OR description LIKE ?)
ORDER BY updated_at DESC
LIMIT 20
""",
(self._user_id, f"%{keyword}%", f"%{keyword}%", f"%{keyword}%"),
).fetchall()

return [
UserPreference(
key=row["key"],
value=row["value"],
category=row["category"],
description=row["description"] or "",
updated_at=row["updated_at"],
)
for row in rows
]
finally:
conn.close()

# ========== 对话历史 ==========
def save_conversation(self, role: str, content: str, metadata: dict[str, Any] | None = None) -> None:
"""保存对话历史。

Args:
role: 角色 (user/assistant/system)
content: 内容
metadata: 额外元数据
"""
import json

conn = sqlite3.connect(str(self._db_path))
try:
conn.execute(
"""
INSERT INTO conversations
(user_id, role, content, timestamp, metadata_json)
VALUES (?, ?, ?, ?, ?)
""",
(
self._user_id,
role,
content,
datetime.now().isoformat(),
json.dumps(metadata or {}, ensure_ascii=False),
),
)
conn.commit()
finally:
conn.close()

def get_recent_conversations(self, limit: int = 20) -> list[dict[str, Any]]:
"""获取最近的对话历史。

Args:
limit: 返回数量限制

Returns:
对话列表
"""
import json

conn = sqlite3.connect(str(self._db_path))
conn.row_factory = sqlite3.Row
try:
rows = conn.execute(
"""
SELECT role, content, timestamp, metadata_json
FROM conversations
WHERE user_id = ?
ORDER BY id DESC
LIMIT ?
""",
(self._user_id, limit),
).fetchall()

result = []
for row in reversed(rows): # 按时间正序返回
result.append({
"role": row["role"],
"content": row["content"],
"timestamp": row["timestamp"],
"metadata": json.loads(row["metadata_json"] or "{}"),
})
return result
finally:
conn.close()


# 全局记忆实例(默认用户)
_global_memory = PersonalMemory()


def get_memory(user_id: str = "default") -> PersonalMemory:
"""获取个人记忆系统实例。

Args:
user_id: 用户 ID,默认 "default"

Returns:
PersonalMemory 实例
"""
if user_id == "default":
return _global_memory
return PersonalMemory(user_id=user_id)

service.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
"""项目九:服务层。

封装全能个人助理 Agent 的核心业务逻辑。
"""
from __future__ import annotations

from typing import Any

from langchain.agents import create_agent

from core import build_chat_model
from core.logging_conf import get_logger

from .memory import get_memory
from .models import AssistantContext, MessageType
from .prompts import ASSISTANT_PROMPT
from .tools import get_all_tools

logger = get_logger("p09.assistant.service")


class PersonalAssistantService:
"""个人助理服务类。

封装个人助理的核心业务逻辑:
- 管理用户上下文和对话历史
- 协调各工具执行用户请求
- 学习和应用用户偏好
- 提供个性化服务
"""

def __init__(self, user_id: str = "default") -> None:
self._user_id = user_id
self._memory = get_memory(user_id)
self._context = AssistantContext(user_id=user_id)
self._agent: Any | None = None

def build_agent(self) -> Any:
"""构建个人助理 Agent。

Returns:
LangChain Agent 对象
"""
if self._agent is not None:
return self._agent

self._agent = create_agent(
model=build_chat_model(),
tools=get_all_tools(),
system_prompt=ASSISTANT_PROMPT,
)
return self._agent

def process_message(self, user_message: str) -> str:
"""处理用户消息。

Args:
user_message: 用户输入消息

Returns:
助理回复
"""
# 保存用户消息到历史
self._context.add_message(MessageType.USER, user_message)
self._memory.save_conversation("user", user_message)

# 检查是否有相关偏好,加入上下文
self._enhance_with_preferences(user_message)

# 构建 Agent 并处理
agent = self.build_agent()

try:
response = agent.run(user_message)

# 保存助理回复到历史
self._context.add_message(MessageType.ASSISTANT, response)
self._memory.save_conversation("assistant", response)

return response
except Exception as e:
logger.error("处理消息失败: %s", e)
error_msg = f"抱歉,处理你的请求时遇到了问题:{e}\n请重试或换一种方式描述。"
self._context.add_message(MessageType.SYSTEM, error_msg)
return error_msg

def _enhance_with_preferences(self, message: str) -> None:
"""用相关的用户偏好增强上下文。

Args:
message: 用户消息
"""
# 简单的关键词匹配查找相关偏好
keywords = ["邮件", "email", "会议", "meeting", "时间", "喜欢", "偏好"]
matched_keywords = [k for k in keywords if k in message]

if matched_keywords:
# 搜索相关偏好
for keyword in matched_keywords:
preferences = self._memory.search_preferences(keyword)
for pref in preferences[:3]: # 最多取 3 个
self._context.set_context(f"pref_{pref.key}", pref.value)

def learn_preference(self, key: str, value: str, description: str = "") -> None:
"""学习用户偏好。

Args:
key: 偏好键
value: 偏好值
description: 描述
"""
self._memory.remember_preference(key, value, description=description)
logger.info("学习用户偏好: %s = %s", key, value)

def get_preference(self, key: str, default: str = "") -> str:
"""获取用户偏好。

Args:
key: 偏好键
default: 默认值

Returns:
偏好值
"""
return self._memory.recall_preference(key, default)

def get_conversation_history(self, limit: int = 20) -> list[dict[str, Any]]:
"""获取对话历史。

Args:
limit: 返回数量限制

Returns:
对话消息列表
"""
return self._memory.get_recent_conversations(limit)

def clear_context(self) -> None:
"""清空当前会话上下文(保留长期记忆)。"""
self._context.clear_short_term()
logger.info("会话上下文已清空")

def generate_daily_summary(self) -> str:
"""生成每日摘要。

Returns:
摘要文本,包含今日日程、待办任务等
"""
from datetime import date

today = date.today().isoformat()

# 获取最近偏好(错误降级,避免依赖未导入符号)
try:
from .tools import list_all_preferences
prefs_summary = list_all_preferences.invoke({})
except Exception:
prefs_summary = "暂无"

summary = f"""
📅 {today} 每日摘要
---

## 今日日程
(调用日历 API 获取)

## 待办任务
(调用任务管理 API 获取)

## 未读邮件
(调用邮件 API 获取)

## 最近偏好
{prefs_summary}

建议:优先处理 P0/P1 任务,重要会议提前准备。
""".strip()

return summary


# 全局服务实例(默认用户)
_service: PersonalAssistantService | None = None


def get_service(user_id: str = "default") -> PersonalAssistantService:
"""获取个人助理服务实例。

Args:
user_id: 用户 ID,默认 "default"

Returns:
PersonalAssistantService 单例
"""
global _service
if _service is None or _service._user_id != user_id:
_service = PersonalAssistantService(user_id)
return _service

project.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
"""项目九:项目定义层。

定义全能个人助理 Agent 的项目注册和对外接口。
"""
from __future__ import annotations

from typing import Any

from core import BaseProject, registry

from .service import get_service
from .tools import get_all_tools


class PersonalAssistantProject(BaseProject):
"""全能个人助理 Agent。

主讲能力:MCP 工具生态 + 短长期记忆 + 多 Skill 调度

业务场景:一个 Agent 统一管理邮箱、日历、文件、笔记、偏好,
像一个真正的私人助理——记住你的喜好、操作你的系统、在你不在时处理事务。

生产级特性:
- MCP 生态接入:日历/邮箱/文件/数据库经 MCP 标准化
- 短长期记忆:短期在线+长期持久化,跨会话记住用户偏好
- 多 Skill 调度:按需加载不同技能
- 统一 API 与 Webhook 回调
- 个性化服务:基于历史偏好提供定制化建议
"""

id = "p09_personal_assistant"
name = "全能个人助理 Agent"
description = "邮箱+日历+文件+偏好,记住你的一切,随取随用。"
capabilities = ["MCP", "Memory", "Skill", "Tool", "Personalization"]

def build_agent(self) -> Any:
"""构建个人助理 Agent 实例。

Returns:
LangChain Agent 对象
"""
service = get_service()
return service.build_agent()

def run(self, message: str) -> str:
"""运行助理任务。

Args:
message: 用户输入的消息

Returns:
助理回复
"""
service = get_service()
return service.process_message(message)


# 项目实例
project = PersonalAssistantProject()

# 对外暴露的快捷函数
run = project.run
build_agent = project.build_agent
get_tools = get_all_tools

__all__ = [
"PersonalAssistantProject",
"project",
"run",
"build_agent",
"get_tools",
]

init.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
"""项目九:全能个人助理 Agent

主讲能力:MCP 工具生态 + 短长期记忆 + 多 Skill 调度

业务场景:一个 Agent 统一管理邮箱、日历、文件、笔记、偏好,
像一个真正的私人助理——记住你的喜好、操作你的系统、在你不在时处理事务。

生产级特性:
- MCP 生态接入:日历/邮箱/文件/数据库经 MCP 标准化
- 短长期记忆:短期在线+长期持久化,跨会话记住用户偏好
- 多 Skill 调度:按需加载不同技能
- 统一 API 与 Webhook 回调
- 个性化服务:基于历史偏好提供定制化建议

架构分层:
- models.py: 数据模型、对象定义
- prompts.py: 系统提示词、模板
- tools.py: 各能力工具函数
- memory.py: 短期/长期记忆系统
- service.py: 业务逻辑、上下文管理
- project.py: 项目定义、对外接口
"""
from __future__ import annotations

from core import registry

# 导出 models 层
from .models import (
AssistantCapability,
AssistantContext,
CalendarEvent,
ConversationMessage,
EmailMessage,
FileSearchResult,
MessageType,
UserPreference,
)

# 导出 prompts 层
from .prompts import (
ASSISTANT_PROMPT,
CALENDAR_PROMPT,
EMAIL_PROMPT,
PREFERENCE_PROMPT,
TASK_PROMPT,
)

# 导出 memory 层
from .memory import PersonalMemory, get_memory

# 导出 tools 层(保持向后兼容)
from .tools import (
check_availability,
create_task,
draft_email,
get_all_tools,
list_all_preferences,
list_tasks,
recall_preference,
remember_preference,
schedule_meeting,
search_files,
search_memories,
send_email,
summarize_file,
)

# 导出 service 层
from .service import PersonalAssistantService, get_service

# 导出 project 层
from .project import (
PersonalAssistantProject,
build_agent,
get_tools,
project,
run,
)

# 向后兼容别名:原单文件版本使用 SYSTEM_PROMPT
SYSTEM_PROMPT = ASSISTANT_PROMPT

# 注册项目到全局注册表
registry.register(project)

__all__ = [
# models
"AssistantCapability",
"MessageType",
"UserPreference",
"CalendarEvent",
"EmailMessage",
"FileSearchResult",
"ConversationMessage",
"AssistantContext",
# prompts
"ASSISTANT_PROMPT",
"SYSTEM_PROMPT",
"EMAIL_PROMPT",
"CALENDAR_PROMPT",
"PREFERENCE_PROMPT",
"TASK_PROMPT",
# memory
"PersonalMemory",
"get_memory",
# tools
"send_email",
"draft_email",
"schedule_meeting",
"check_availability",
"search_files",
"summarize_file",
"remember_preference",
"recall_preference",
"search_memories",
"list_all_preferences",
"create_task",
"list_tasks",
"get_all_tools",
# service
"PersonalAssistantService",
"get_service",
# project
"PersonalAssistantProject",
"project",
"run",
"build_agent",
"get_tools",
]

11.4.2 核心代码讲解

短期记忆与长期记忆的双层设计

PersonalMemorymemory.py)一手管着两类记忆。短期记忆基于进程内字典 self._short_term,只在当前会话活着,会话一结束就随风而散,适合放临时上下文和中间状态——就像助理桌上那张便签,下班就清掉。长期记忆则通过 SQLite 落盘,能跨会话保留用户偏好与历史对话——就像助理随身带的小本本,关机再开机,"老板花生过敏"这条还在。

这套双层设计有个好处:快的归快、久的归久。短期记忆直接走内存,响应飞快;长期记忆走磁盘,慢一点但靠得住。两者配合,既不丢长期习惯,又不让每次回答都被磁盘拖慢。它也顺手解决了纯上下文方案的两个老毛病——会话结束即失忆、context 越拉越长越贵。

SQLite 持久化的偏好/对话/任务存储

_init_db 在初始化时一次性建好三张表:preferences(用户偏好)、conversations(对话历史)、tasks(任务)。注意这三张表是分而治之的——偏好是"精选记忆",对话和任务是"流水账",三者各占一张表,互不污染。

写操作上,所有 SQL 都用参数化占位符(?),杜绝注入;偏好更新走 INSERT OR REPLACE,同 key 直接覆盖,省去"先查后改"的来回。读操作上,用 sqlite3.Row 把行映射成字段对象,再转成 UserPreference 这类领域模型——数据库里是行,代码里是对象,边界干净。search_preferences 在 key、value、description 三个字段上做 LIKE 模糊匹配,这就是本章记忆检索的"初级检索器"。

💡 为什么选 SQLite?因为它零部署、单文件、跟项目一起走。个人助理这种轻量持久化场景,上 PostgreSQL 属于杀鸡用牛刀;等到多用户、高并发那天再迁移也不迟,反正数据访问都被封装在记忆层里了。

MCP 思维与工具标准化

tools.py 用 LangChain 的 @tool 装饰器,把邮件、日历、文件、记忆、任务这些能力统一封装成标准工具——每个工具都有类型化的入参、清晰的 docstring 和确定的返回值,对外长得一模一样。get_all_tools() 再把所有工具收拢成一个清单交给 Agent 调度。

这里要重申一遍本章的核心主张:我们不是在教你调某个具体 SaaS API,而是在演示**“通过标准接口把私人助理接入外部世界”**这条路。生产环境里,这些本地函数可以逐个被替换成邮箱、日历、文件系统等 MCP Server,而 Agent 调用它们的方式分毫不变。换句话说,工具是"可换的零件",助理的"大脑"和"调度逻辑"是"不动的底盘"——这正是 MCP 思维要带给你的解耦红利。

服务层的上下文增强(基于偏好个性化)

PersonalAssistantService.process_message 在把请求交给 Agent 之前,先悄悄做一件"功课"——_enhance_with_preferences:用一组关键词(邮件、会议、时间、喜欢、偏好……)去扫用户消息,一旦命中,就触发 search_preferences 检索相关长期记忆,再把命中的偏好塞进 AssistantContextactive_context。这样后续回答就能自然带出"根据你的偏好,会议室已订 A101……"这种个性化表述。

这一步的精髓在于:记忆不是存起来就完了,得在回答前主动捞出来用。存而不取,等于没记。同时,用户消息和助理回复会双写到上下文与 SQLite,留下可追溯的痕迹。

⚠️ 坦白讲,目前的"关键词匹配 + LIKE 检索"还比较粗——它够用来演示"个性化"这条链路是通的,但离生产级精准还差一层向量召回。这是本章有意留出的演进空间,读者可以把它当作练习题。

错误降级处理

助理替你办事,难免有失手的时候——工具调不通、模型推理挂了、网络抽风。process_message 用 try/except 把 Agent 执行整体兜住:一旦出错,不把原始堆栈甩到用户脸上,而是记日志 + 返回一条友好的降级提示,同时把错误信息作为 system 消息写进上下文,方便事后复盘。recall_preference 这类读取工具在偏好不存在时也会返回明确的提示文本,而不是静默返回空。

这种"失败可见、可恢复"的设计,是个人助理类应用的基本修养。道理很简单:助理是替你伸手的人,它出错你不怕,怕的是它出错了你都不知道。

三层架构的解耦优势

项目按模型层(models.py)、提示词层(prompts.py)、工具层(tools.py)、记忆层(memory.py)、服务层(service.py)、项目层(project.py)分层,入口 init.py 负责统一注册与 re-export。模型与存储分家、工具与提示词分家、业务逻辑与对外接口分家——这"三个分家"带来三个实打实的好处:

一是单层可独立测试与替换。比如把模拟工具换成真实 MCP Server,服务层一行不用动;二是持久化逻辑集中。偏好/对话/任务的存储都收在记忆层,服务层只编排不存储,职责干净不串味;三是对外接口统一project.py 通过 BaseProject 暴露统一的 build_agent/run,全局注册表和上层框架能一视同仁地调度它,不用关心底下是哪个项目。

💡 一句话:分层不是为了好看,是为了"换零件不动底盘"。个人助理又是最容易频繁换零件的那种系统,所以这层解耦对它尤其值钱。


11.5 跑一跑:它真的行吗

个人助理是"替你伸手"的系统,伸错手的代价很高,所以测试尤其不能糊弄。本项目的测试套件(backend/projects/p09_personal_assistant/test_agent.py)覆盖:

  • 短期记忆读写——便签写得进、读得出
  • 长期记忆持久化——关掉进程再开,偏好还在
  • 记忆搜索——关键词能命中该命中的
  • 邮件/日历工具——工具的入参出参契约稳定
  • 集成测试(需 API Key)——端到端跑通"用户说话→助理回复"

离线的领域模型与记忆测试不依赖外部服务,随时能跑;涉及真实模型推理的集成测试由 @pytest.mark.skipif 门控(测试套件按 ANTHROPIC_API_KEY 是否存在决定跳过),按需开启。


11.6 送上线:让它上班

助理一旦上线,就不再是"模拟"了——它发的邮件真的会发出去,它订的会议真的会出现在别人日历上,它记的偏好真的会长期留着。权力一大,约束就必须跟上。上线私人助理类应用时,这几个问题得先答清楚:

  • 发送邮件前是否需要人工确认?(误发一封含敏感信息的邮件,撤不回来)
  • 删除文件是否禁止?(助理不该有"删"的权力,或至少要二次确认)
  • 日历邀请是否会打扰他人?(替老板发会议邀请,等于以老板之名动用他人时间)
  • 长期记忆是否允许用户查看和删除?(“被记住"也得能"被遗忘”)

⚠️ 避坑:权限白名单、敏感操作人工确认、记忆治理,这三件套一个都不能少。 生产级系统必须提供权限白名单(哪些工具允许自动执行、哪些必须拦下来)、人工确认(涉及费用/权限/外发/删除的操作,先问人再动手)、记忆管理页面(用户能看、能改、能删自己的长期记忆)。

⚠️ 记忆治理尤其容易被忽视。助理记下的偏好里可能混进身份证、口令、医疗信息这类敏感数据,如果不给用户"查看与删除"的入口,就等于在数据库里埋了一颗隐私地雷——GDPR/《个人信息保护法》一类法规第一个找的就是你。


11.7 回头看:学到了什么

回到开头那位换过三个助理的老板。他要的从来不是"会做事"的人,而是"知道他是什么人"的人。本章造的助理,正是在朝这个方向努力:用 MCP 思维让它能伸手够到外部世界,用短长期记忆让它记住你是谁,用上下文增强让回答带上你的偏好,用分层架构让这一切可演进。

个人助理的核心不是工具多,而是工具 + 记忆 + 权限三者平衡。

没有记忆的助理不贴心,没有权限控制的助理不安全;记忆让它懂你,权限让它不害你。

下一章,我们再把规划、反思、记忆和工具全部融合到一起,造一个能自己拆任务、自己回头改错的自主任务规划 Agent。


如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !