AICompanion 开发日志 04:真实模型接入诊断与情感交互方向校准
上一篇日志整理了 AICompanion 当前已经实现的功能:PySide6 聊天窗口、SQLite 本地存储、长期记忆、情绪状态、主动消息雏形,以及 OpenAI-compatible 大模型接口。
写完之后,我继续往前推进了一小步。我开始把项目从本地 mock 回复,推进到更稳定地接入真实大模型。
同时我也重新校准了一下项目定位。AICompanion 不是要做成一个传统桌面助手,不是以任务管理、效率工具或系统控制为核心。它最终想实现的是一个基于 Live2D 动漫角色的情感交互应用。也就是说,后续所有模块都应该服务于“角色感”“陪伴感”“情绪反馈”和“长期关系感”。
1. 这次新增内容概览
这次主要新增和完善的是大模型接入体验,集中在 llm 模块和 README 文档中。
当前相关文件包括:
src/aicompanion/llm/
base.py
mock_client.py
openai_compatible_client.py
factory.py
diagnostics.py
其中这次比较重要的是:
src/aicompanion/llm/diagnostics.py
src/aicompanion/llm/openai_compatible_client.py
README.md
也就是说,现在项目不只是“代码里有一个真实模型客户端”,而是有了一个单独的诊断入口,可以在不启动桌面窗口的情况下,先测试模型配置是否正确。
这对后续开发很重要。因为如果每次测试模型都要打开 GUI,再输入消息,再看窗口里是否报错,调试效率会很低。现在可以先在命令行里确认模型连通性,再回到 PySide6 界面里测试完整交互流程。
2. 新增 LLM 诊断命令
新增的诊断工具位于:
src/aicompanion/llm/diagnostics.py
它的作用是:在不打开桌面 UI 的情况下,直接读取 .env 配置,创建当前配置对应的 LLM 客户端,然后发送一条测试消息。
README 中也补充了使用方式:
.\.venv\Scripts\python.exe -m aicompanion.llm.diagnostics
如果想发送自定义测试消息,可以这样写:
.\.venv\Scripts\python.exe -m aicompanion.llm.diagnostics "你好,请简单介绍一下你自己"
默认情况下,诊断命令会测试流式输出。也就是模型返回的内容会一段一段打印出来,模拟正式聊天窗口里的打字效果。
如果想测试非流式回复,可以加上:
.\.venv\Scripts\python.exe -m aicompanion.llm.diagnostics --no-stream
这个工具的价值不在于功能复杂,而在于它把“模型接入是否正常”从 GUI 流程里拆了出来。后续无论换 OpenAI-compatible 平台、换模型名、换 base_url,还是调整 API key,都可以先用这个命令做一次最小验证。
3. 诊断工具的执行流程
diagnostics.py 的流程很简单:
读取命令行参数
-> 加载 .env 配置
-> 打印 provider / base_url / model / streaming
-> create_llm_client(settings)
-> 构造一条测试消息
-> 调用 generate_reply_stream() 或 generate_reply()
-> 打印模型回复
核心逻辑大致是:
settings = load_settings()
client = create_llm_client(settings)
messages = [ChatTurn(role="user", content=args.message)]
for token in client.generate_reply_stream(
messages=messages,
system_prompt=system_prompt,
):
print(token, end="", flush=True)
这里没有复用正式聊天窗口里的完整角色 prompt,而是写了一个更小的诊断 prompt:
You are a warm AI companion. Reply naturally in Chinese, and keep the answer concise.
这样做是为了让诊断工具保持轻量。它主要验证 API 是否能通、流式解析是否正常、模型是否能返回中文内容,而不是完整测试角色人格。
完整角色设定仍然在 ChatController._build_system_prompt() 中负责构建。
4. 流式和非流式都可以单独测试
当前项目的 LLM 抽象接口本来就同时支持两种模式:
def generate_reply(self, messages, system_prompt) -> str:
...
def generate_reply_stream(self, messages, system_prompt):
...
桌面窗口默认使用流式回复,因为这样更像角色正在一句一句说话,而不是突然弹出一整段文本。
但是非流式接口仍然有保留价值:
- 方便快速测试模型是否可用。
- 方便后续写自动化测试。
- 适合某些不支持流式输出的平台。
- 后续如果做“记忆摘要”“情绪分析”等内部任务,非流式会更简单。
因此这次诊断工具同时支持两种模式。这样后续排查问题时,可以判断问题到底出在普通 API 调用,还是出在 SSE 流式解析。
5. API 错误处理进一步清晰化
真实 API 接入和 mock 最大的区别是:真实 API 会失败,而且失败原因很多。
可能是:
- API key 没填。
- API key 错误或过期。
- base_url 写错。
- 模型名不存在。
- 平台余额不足。
- 请求被限流。
- 网络或代理异常。
- 平台返回的流式数据格式不符合预期。
如果把这些底层异常直接抛给 UI,用户看到的可能是 urllib.error.HTTPError、URLError、JSONDecodeError 之类的信息,既不友好,也不利于判断下一步该改哪里。
所以 openai_compatible_client.py 中增加了统一异常:
class LLMResponseError(RuntimeError):
...
它的目的不是隐藏错误,而是把底层错误整理成更容易理解的提示。
例如 HTTP 状态码会被转换成更明确的排查方向:
| 状态码 | 提示方向 |
|---|---|
| 401 | 检查 API key 是否正确 |
| 403 | 检查账号权限、模型权限或访问限制 |
| 404 | 检查 base_url 或接口路径 |
| 429 | 检查限流、余额或配额 |
| 500+ | 平台服务端可能异常 |
这样当桌面窗口请求失败时,UI 显示的就不是一大串 traceback,而是相对可读的错误说明。
6. 流式响应解析的处理
OpenAI-compatible 的流式输出通常是 SSE 格式,大致长这样:
data: {"choices":[{"delta":{"content":"你好"}}]}
data: {"choices":[{"delta":{"content":"呀"}}]}
data: [DONE]
当前客户端会逐行读取响应:
for raw_line in response:
line = raw_line.decode("utf-8", errors="ignore").strip()
然后只处理 data: 开头的内容。
如果遇到:
data: [DONE]
就结束本次流式响应。
如果是普通 JSON 事件,就从里面取:
choices[0].delta.content
这个过程现在已经被封装在 _parse_stream_token() 中。这样后续如果某个平台的兼容格式有细微差异,也可以集中在这里调整。
7. 这一步和情感交互有什么关系
表面上看,这次新增的是命令行诊断、错误处理和 API 调试工具,好像和“情感”没什么关系。
但对 AICompanion 来说,这其实是情感交互的地基。
因为一个情感交互角色至少需要稳定做到几件事:
- 能持续对话。
- 能自然地逐段回复。
- 出错时不让整个体验突然崩掉。
- 能根据角色设定、情绪状态和长期记忆来组织语言。
- 后续能把文本回复同步给 Live2D 表情、动作和 TTS。
如果模型调用本身不稳定,后面的 Live2D、语音、表情、记忆都会被影响。
所以当前阶段不是在做“桌面助手能力”,而是在补齐一个情感角色应用必须有的对话基础设施。
换句话说,现在的目标不是让 AICompanion 帮我完成多少任务,而是让她能更稳定、更自然、更有角色感地回应我。
8. 项目方向重新校准
之前代码中已经预留了 reminders 表,也有主动消息雏形。但这不代表项目要走传统助手路线。
我现在更明确地把 AICompanion 的方向定为:
Live2D 动漫角色 + 大模型对话 + 长期记忆 + 情绪状态 + 语音与动作反馈
它更接近一个情感交互应用,而不是效率工具。
因此后续功能排序也应该围绕“情绪反馈”和“角色表现”展开。
例如,长期记忆不只是为了记住任务,而是为了让角色形成关系连续性:
- 记得用户喜欢什么。
- 记得用户讨厌什么。
- 记得用户最近在做什么。
- 记得用户常见的情绪状态。
- 在合适的时候自然提起,而不是机械复述。
情绪系统也不只是 prompt 里的几个数字,而应该逐步驱动:
- 回复语气。
- 主动关心的频率。
- Live2D 表情。
- Live2D 动作。
- TTS 的语速、音量和情绪。
这才是项目后续真正要生长的方向。
9. 当前已经推进到什么程度
结合上一篇日志和这次新增内容,现在项目状态可以更新为:
| 模块 | 当前状态 |
|---|---|
| PySide6 聊天窗口 | 已实现基础版本 |
| 后台线程流式回复 | 已实现 |
| 本地 mock 模型 | 已实现 |
| OpenAI-compatible 客户端 | 已实现 |
| LLM 命令行诊断工具 | 已新增 |
| 流式/非流式模型测试 | 已新增 |
| API 错误提示整理 | 已增强 |
| SQLite 聊天记录 | 已实现 |
| 长期记忆 | 仍是关键词规则 |
| 情绪状态 | 已有基础数值模型 |
| 主动消息 | 仍是规则和固定文本 |
| Live2D | 未实现 |
| TTS | 未实现 |
也就是说,这次没有大幅增加新的用户可见功能,但增强了真实模型接入的可调试性和可靠性。
这一步完成之后,后面继续做情感层能力会更踏实。
10. 下一步计划
接下来我准备把重点放回“情感交互”本身,而不是扩展成桌面助手。
更适合下一阶段的方向是:
- 优化角色 prompt,让回复更稳定地符合 Emilia 的性格设定。
- 改造长期记忆,不再只保存原句,而是提取成适合角色理解的摘要。
- 设计情绪状态到角色表现的映射,比如
happy、lonely、calm分别对应什么表情和语气。 - 为 Live2D 接入提前设计角色状态接口,而不是一上来就把模型显示出来。
- 后续再接入 TTS,让文本回复逐步变成“说话”。
对 AICompanion 来说,Live2D 不是单独的展示层,而应该是情感状态的外显。
所以在真正接入 Live2D 之前,我需要先让内部状态更清楚:角色现在是什么心情,为什么变成这个心情,她应该用什么语气回应,又应该用什么表情和动作表现出来。
这一篇日志记录的就是这个过渡阶段:从“能聊天”走向“更稳定地接入真实模型”,再继续朝“能形成情感反馈的角色”前进。
