AICompanion 开发日志 03:当前版本功能总览
前两篇分别记录了项目搭建和 PySide6 聊天窗口的实现。这一篇不继续单独展开某一个模块,而是做一次阶段性整理:把目前代码里已经实现的功能完整梳理一遍。
AICompanion 现在还不是最终形态,还没有 TTS,也还没有 Live2D。但它已经不只是一个空窗口了。目前这个版本已经把桌面聊天、模型接口、本地存储、长期记忆、情绪状态和主动消息的基础流程都接了起来。
1. 当前项目状态
当前项目路径是:
D:\Study\AICompanion
项目采用 src layout,核心代码放在:
src/aicompanion/
目前主要模块如下:
src/aicompanion/
app.py # 应用装配入口
config.py # 配置读取
controllers/
chat_controller.py # 聊天流程控制
domain/
character.py # 角色设定
emotion.py # 情绪状态
llm/
base.py # LLM 统一接口
mock_client.py # 本地模拟模型
openai_compatible_client.py
factory.py # 根据配置创建模型客户端
services/
memory_service.py # 长期记忆服务
proactive_service.py # 主动消息定时服务
storage/
schema.py # SQLite 表结构
database.py # SQLite 数据访问层
ui/
main_window.py # PySide6 主窗口
chat_worker.py # 后台线程工作对象
这套结构的目标是:UI 只管界面,Controller 负责流程,Service 负责领域功能,Storage 负责数据库,LLM 层负责模型调用。
2. 应用启动流程
程序入口依然是根目录下的:
main.py
它只做一件事:调用 aicompanion.app.main()。
真正的应用装配在:
src/aicompanion/app.py
启动时会完成这些步骤:
- 读取
.env和系统环境变量。 - 创建
QApplication。 - 初始化 SQLite 数据库。
- 创建
MemoryService。 - 根据配置创建 LLM 客户端。
- 创建
ChatController。 - 创建
MainWindow。 - 启动主动消息定时服务。
- 显示窗口。
对应代码逻辑大致是:
settings = load_settings()
app = QApplication(sys.argv)
database = Database(settings.database_path)
database.initialize()
memory_service = MemoryService(database)
llm_client = create_llm_client(settings)
controller = ChatController(database, memory_service, llm_client)
window = MainWindow(controller=controller)
proactive_service = ProactiveMessageService(controller=controller, parent=window)
proactive_service.message_ready.connect(window.append_assistant_message)
proactive_service.start()
window.show()
这里的 app.py 不写具体业务逻辑,只负责把各个模块接起来。这样后续如果要替换模型、换数据库实现、加新服务,都不会影响启动入口太多。
3. 配置系统
项目使用 python-dotenv 读取 .env。
配置集中在:
src/aicompanion/config.py
目前支持的配置项包括:
APP_NAME
DATABASE_PATH
LLM_PROVIDER
LLM_BASE_URL
LLM_API_KEY
LLM_MODEL
LLM_TEMPERATURE
LLM_TIMEOUT_SECONDS
对应的默认开发配置是:
APP_NAME=AICompanion
DATABASE_PATH=data/aicompanion.sqlite3
LLM_PROVIDER=mock
LLM_BASE_URL=https://api.openai.com/v1
LLM_API_KEY=
LLM_MODEL=gpt-4o-mini
LLM_TEMPERATURE=0.8
LLM_TIMEOUT_SECONDS=60
这里比较重要的是 LLM_PROVIDER。
当前支持:
mock:本地模拟回复,不需要 API key。openai_compatible:调用 OpenAI-compatible Chat Completions 接口。zhipu_compatible:目前也走兼容接口。
默认使用 mock,所以即使没有 API Key,也可以先开发窗口、数据库和消息流程。
4. PySide6 聊天窗口
当前桌面界面由 MainWindow 实现:
src/aicompanion/ui/main_window.py
窗口已经实现了这些基础功能:
- 显示标题
AICompanion。 - 显示聊天记录列表。
- 提供输入框。
- 提供发送按钮。
- 支持回车发送。
- 用户消息靠右显示。
- AI 消息靠左显示。
- 发送中禁用输入框和按钮。
- 回复结束后恢复输入。
- 启动时加载最近聊天记录。
目前聊天列表使用 QListWidget,每一条消息是一个 QListWidgetItem:
item = QListWidgetItem(f"{sender}: {content}")
item.setTextAlignment(
Qt.AlignmentFlag.AlignRight if align_right else Qt.AlignmentFlag.AlignLeft
)
self.chat_list.addItem(item)
这个版本的 UI 还比较朴素,但已经能支撑完整聊天流程。后面可以继续升级为气泡样式、头像、角色立绘区域,最终再接 Live2D。
5. 后台线程与流式回复
聊天程序不能在主线程里直接请求模型。
如果模型 API 比较慢,而请求又发生在 PySide6 主线程中,窗口就会卡住。所以当前项目加入了:
src/aicompanion/ui/chat_worker.py
ChatWorker 运行在 QThread 中,负责调用:
self.controller.stream_user_message(self.user_message)
它通过 Signal 把结果发回 UI:
token_received = Signal(str)
finished = Signal()
failed = Signal(str)
主窗口收到 token 后,会逐段更新当前 AI 消息:
self.current_assistant_text += token
self.current_assistant_item.setText(
f"Emilia: {self.current_assistant_text}"
)
这就形成了类似“AI 正在打字”的流式显示效果。
即使当前使用的是 mock 模型,也会把回复拆成小片段返回,用来模拟真实 API 的流式输出。
6. 聊天流程控制器
聊天流程集中在:
src/aicompanion/controllers/chat_controller.py
这是目前项目中比较核心的模块。
用户发送一条消息后,完整流程大致如下:
MainWindow
-> ChatWorker(QThread)
-> ChatController.stream_user_message()
-> 保存 user 消息到 SQLite
-> 更新情绪状态
-> 尝试提取长期记忆
-> 读取最近聊天记录
-> 构建 system prompt
-> 调用 LLMClient.generate_reply_stream()
-> UI 逐段显示 token
-> 保存完整 assistant 回复到 SQLite
也就是说,Controller 不只是转发消息,它还负责把记忆、情绪、历史记录和大模型调用串成一个完整流程。
当前 Controller 里保留了两个发送接口:
send_user_message():同步生成完整回复。stream_user_message():流式生成回复,桌面 UI 默认使用这个。
同步接口可以留给后续测试或命令行调试使用。
7. LLM 抽象与本地 mock
大模型接口定义在:
src/aicompanion/llm/base.py
其中定义了两个核心概念:
@dataclass(frozen=True)
class ChatTurn:
role: str
content: str
以及统一的模型客户端接口:
class LLMClient(Protocol):
def generate_reply(self, messages, system_prompt) -> str:
...
def generate_reply_stream(self, messages, system_prompt):
...
这样 UI 和 Controller 不需要关心具体模型供应商。
当前本地 mock 客户端在:
src/aicompanion/llm/mock_client.py
它会读取最近一条用户消息,然后返回模拟回复:
我听到啦。你刚才说:...
现在我还是本地模拟回复,下一步我们会把这里替换成真正的大模型 API。
mock 客户端的价值很大:
- 不需要 API Key。
- 不需要网络。
- 调试 UI 更方便。
- 可以验证数据库保存流程。
- 可以模拟流式输出。
这让项目在早期不依赖真实模型服务,也能继续开发。
8. OpenAI-compatible API 客户端
真实模型调用代码在:
src/aicompanion/llm/openai_compatible_client.py
它调用的是常见的 Chat Completions 兼容接口:
POST {LLM_BASE_URL}/chat/completions
请求体中会包含:
modelmessagestemperaturestream
消息列表会先放入一条系统提示词:
api_messages = [{"role": "system", "content": system_prompt}]
再追加最近的用户和 assistant 历史消息。
当前客户端支持两种模式:
- 非流式:
generate_reply() - 流式:
generate_reply_stream()
流式响应按 SSE 格式解析,遇到:
data: [DONE]
就结束输出。
项目这里暂时没有引入额外 SDK,而是使用 Python 标准库 urllib。这样依赖更少,也更容易看清楚一次模型请求到底发出了什么。
9. SQLite 本地存储
数据库表结构定义在:
src/aicompanion/storage/schema.py
当前已经有四张表:
chat_messages
memories
reminders
emotion_state
其中:
| 表名 | 作用 |
|---|---|
chat_messages |
保存用户和 AI 的聊天记录 |
memories |
保存长期记忆 |
reminders |
保存任务提醒,当前先预留表结构 |
emotion_state |
保存角色情绪状态 |
数据库访问集中在:
src/aicompanion/storage/database.py
目前已经实现:
- 初始化表结构。
- 初始化默认情绪状态。
- 保存聊天消息。
- 读取最近聊天消息。
- 保存长期记忆。
- 读取长期记忆。
- 读取情绪状态。
- 保存情绪状态。
还有一个细节是:Database.connect() 每次都会创建新的短生命周期连接。
这是为了避免在 UI 主线程和后台线程之间共享同一个 sqlite3.Connection。桌面程序里一旦涉及线程,共享数据库连接很容易出现奇怪问题,所以这里提前规避。
10. 长期记忆系统
长期记忆服务在:
src/aicompanion/services/memory_service.py
当前版本先采用非常简单的关键词规则。
如果用户消息中包含这些词:
我喜欢
我讨厌
记住
我的名字
我正在
就把整条用户消息保存进 memories 表。
例如用户输入:
记住,我喜欢晚上写代码。
这条消息就会被保存为长期记忆。
构建 prompt 时,会读取最近的重要记忆:
memories = self.database.recent_memories()
然后整理成:
- 记住,我喜欢晚上写代码。
- 我的名字是 ...
这些内容会放进系统提示词,让模型在回复时可以参考。
当前记忆系统还很朴素,但已经有了最小闭环:
用户说出重要信息
-> 规则命中
-> 保存到 SQLite
-> 下次构建 prompt 时注入长期记忆
后续可以升级为 LLM 自动总结,或者加入向量检索。
11. 角色设定
角色设定在:
src/aicompanion/domain/character.py
目前定义了一个 CharacterProfile:
@dataclass(frozen=True)
class CharacterProfile:
name: str
description: str
speaking_style: str
默认角色名是:
Emilia
这里需要注意:当前代码中的设定说明是“原创陪伴角色”,并明确要求不要声称自己是受版权保护的角色。
角色设定目前会进入系统提示词:
Character name: Emilia
Character description: ...
Speaking style: ...
后续这个模块可以继续扩展:
- Live2D 模型路径。
- 不同情绪对应的表情。
- 角色口癖。
- 不同亲密度阶段的说话方式。
12. 情绪状态系统
情绪状态定义在:
src/aicompanion/domain/emotion.py
当前包含四个字段:
mood: str = "calm"
affection: int = 50
loneliness: int = 15
energy: int = 80
含义大致是:
| 字段 | 含义 |
|---|---|
mood |
当前心情 |
affection |
亲密度 |
loneliness |
孤独感 |
energy |
精力 |
当用户发送消息后:
self.loneliness = max(0, self.loneliness - 10)
self.affection = min(100, self.affection + 1)
self.energy = max(0, self.energy - 1)
self.mood = "happy" if self.affection >= 55 else "calm"
也就是:
- 孤独感下降。
- 亲密度略微上升。
- 精力略微下降。
- 亲密度到一定程度后心情变为
happy。
当用户长时间没有回复时:
self.loneliness = min(100, self.loneliness + 5)
if self.loneliness >= 60:
self.mood = "lonely"
也就是孤独感会上升,并可能切换到 lonely 状态。
这些情绪值目前已经会保存到 SQLite,并且会进入系统提示词:
Current emotion:
- mood: calm
- affection: 50
- loneliness: 15
- energy: 80
后续它可以影响:
- 回复语气。
- 主动消息频率。
- Live2D 表情。
- TTS 声音参数。
13. 主动消息雏形
主动消息服务在:
src/aicompanion/services/proactive_service.py
它使用 QTimer,每 60 秒检查一次:
self.timer.setInterval(60_000)
定时器本身不决定说什么,而是调用:
self.controller.maybe_create_proactive_message()
当前主动消息触发条件是规则式的:
- 用户至少说过一句话。
- 最近 30 分钟没有用户消息。
- 最近 30 分钟没有发过主动消息。
- 情绪状态里的
loneliness达到一定阈值。
如果条件满足,就会生成一条固定主动消息,并保存到数据库:
你这完了吗?我刚刚有点想你。不过不用着急,我会在这里等你。
然后通过 Signal 发给窗口显示。
这个功能现在还只是雏形,但它已经证明了主动消息的基本链路:
QTimer 定时触发
-> Controller 判断规则
-> 更新情绪状态
-> 生成主动消息
-> 保存到 SQLite
-> MainWindow 显示
后续可以把固定文本升级为 LLM 生成,让角色根据当前情绪和记忆主动说更自然的话。
14. 系统提示词构建
当前每次请求模型时,Controller 都会构建系统提示词。
系统提示词由几部分组成:
- 角色名。
- 角色描述。
- 说话风格。
- 当前情绪。
- 长期记忆。
大致结构是:
Character name: Emilia
Character description: ...
Speaking style: ...
Current emotion:
- mood: calm
- affection: 50
- loneliness: 15
- energy: 80
Long-term memories:
- ...
这一步很关键。
因为 AICompanion 不是单纯把用户输入发给模型,而是把“角色是谁”“现在状态如何”“记得用户什么事情”一起发给模型。
这样后续的陪伴感、角色感、长期记忆和情绪变化,才有地方发挥作用。
15. 当前已经完成的功能清单
截至目前,代码里已经实现的功能可以整理为:
| 功能 | 当前状态 |
|---|---|
| PySide6 桌面窗口 | 已实现 |
| 聊天列表显示 | 已实现 |
| 输入框和发送按钮 | 已实现 |
| 回车发送 | 已实现 |
| 用户/AI 消息左右区分 | 已实现 |
| 启动时加载历史记录 | 已实现 |
| 后台线程请求回复 | 已实现 |
| 流式 token 显示 | 已实现 |
| 本地 mock 模型 | 已实现 |
| OpenAI-compatible API 客户端 | 已实现 |
| 非流式回复接口 | 已实现 |
| 流式回复接口 | 已实现 |
.env 配置读取 |
已实现 |
| SQLite 表结构 | 已实现 |
| 聊天记录保存 | 已实现 |
| 长期记忆保存 | 已实现 |
| 长期记忆注入 prompt | 已实现 |
| 情绪状态保存 | 已实现 |
| 用户消息后更新情绪 | 已实现 |
| 久未回复时提升孤独感 | 已实现 |
| 主动消息定时检查 | 已实现 |
| 主动消息显示 | 已实现 |
| 任务提醒表结构 | 已预留 |
| TTS 语音回复 | 未实现 |
| Live2D 展示 | 未实现 |
从这个表可以看出,项目目前已经越过了“单纯聊天窗口”的阶段,进入了“桌面 AI 陪伴系统雏形”的阶段。
16. 当前版本的不足
虽然基础流程已经打通,但现在还有不少明显不足:
- 聊天 UI 还比较简单,没有气泡、头像和富文本。
- 长期记忆只是关键词规则,容易漏掉重要信息。
- 主动消息还是固定文本,不够自然。
- 情绪系统只影响 prompt,还没有真正驱动表情或语音。
reminders表已经有了,但任务提醒功能还没有完整实现。- API 客户端还没有做重试、取消请求和更细的错误展示。
- 还没有 TTS。
- 还没有 Live2D。
不过这些不足并不是坏事。现在最重要的是,项目的各个基础模块已经有了明确位置,后面可以逐步替换和增强。
17. 下一步计划
接下来我准备继续完善两个方向。
第一个方向是大模型接入体验:
- 更详细记录 OpenAI-compatible API 的接入过程。
- 测试真实模型回复。
- 优化流式响应错误处理。
- 让角色回复更符合设定。
第二个方向是本地存储和记忆:
- 更完整地讲 SQLite 表设计。
- 给记忆增加去重或更新逻辑。
- 让长期记忆不只是保存原句,而是变成更适合 prompt 的摘要。
- 后续加入任务提醒的增删查改。
到目前为止,AICompanion 已经有了一个可以继续生长的核心骨架。接下来每一步功能增强,都可以围绕这个骨架慢慢叠上去。
