一、架构总览
CLI 模式(单进程同步)
┌──────────────────────────────────────────┐
│ HermesCLI (一个进程) │
│ ┌─────────────┐ 直接调用 ┌──────────┐ │
│ │ cmdloop() │ ────────> │ AIAgent │ │
│ │ read-eval │ <──────── │ run_conv │ │
│ │ -print │ 同步返回 │ │ │
│ └─────────────┘ └──────────┘ │
│ 一个进程 = 一个会话 │
└──────────────────────────────────────────┘
Telegram Gateway 模式(异步事件驱动)
┌─────────────────────────────────────────────────────────────┐
│ Gateway (asyncio, 多会话复用) │
│ │
│ 会话A ← Telegram适配器 ← 用户1 │
│ 会话B ← Telegram适配器 ← 用户2 ┌───────────────────────┐ │
│ 会话C ← Discord适配器 ← 用户3 │ AIAgent (线程池) │ │
│ ... │ run_conversation() │ │
│ ────────> │ 每次消息 fork 线程执行 │ │
│ <──────── │ │ │
│ 中断机制: 新消息 → interrupt_event → 旧轮停止 │
└─────────────────────────────────────────────────────────────┘
二、逐项对比
| 维度 | CLI | Telegram Gateway |
|---|---|---|
| 进程模型 | 单进程同步 REPL | asyncio 事件循环 + 线程池 |
| 会话管理 | 1 CLI 进程 = 1 会话 | 1 Gateway 管理 N 会话 × M 平台 |
| 消息处理 | 直接调 run_conversation() | _process_message_background() → 异步编排 |
| 中断机制 | 无(必须等当前轮完成) | 新消息 → interrupt_event → 旧轮停止 |
| 工具过滤 | 全部可用 | _get_platform_tools() 按平台过滤 |
| 正在输入提示 | 无 | _keep_typing() 每 2 秒刷新 |
| 消息投递 | print() 到 stdout | 平台适配器的 send_message() |
| 多用户共享 | 单用户交互 | 群组 topic 模式下多人可参与同一会话 |
| tool progress | 内联 ANSI 渲染 | 消息编辑模式 |
| session 持久化 | SessionStore (SQLite) | 同上,共用同一套引擎 |
三、核心引擎相同
CLI 和 Telegram 共享同一套核心引擎:
run_conversation()— agent/conversation_loop.pyAIAgent— run_agent.py- 同一套 tool call 执行引擎
- 同一套 context compression
- 同一套 SQLite session 存储
- 同一套 skill/memory 系统
差异仅在交互层:消息来源不同(stdin vs Telegram webhook)、投递方式不同(print vs send_message)、中端机制不同(无 vs interrupt_event)。
四、会话隔离规则(源码级)
build_session_key 源码
# 文件: gateway/session.py, 第 600-665 行
def build_session_key(source, group_sessions_per_user=True, thread_sessions_per_user=False):
# DM: 按 chat_id 隔离
if source.chat_type == "dm":
return f"agent:main:{platform}:dm:{dm_chat_id}"
# 非 DM: 关键规则
isolate_user = group_sessions_per_user # 默认 True
if source.thread_id and not thread_sessions_per_user: # thread 默认共享
isolate_user = False # ← 不隔离!所有人共享
if isolate_user and participant_id:
key_parts.append(str(participant_id)) # 仅非 thread 时加 user_id
return ":".join(key_parts)
实际 session_key 示例
| 场景 | session_key 格式 |
|---|---|
| CLI 默认 | agent:main:cli |
| Telegram DM | agent:main:telegram:dm:-100xxxx |
| Telegram 群组(非 topic) | agent:main:telegram:group:-100xxxx:user_id_xxx(每人独立) |
| Telegram 群组 topic | agent:main:telegram:group:-100xxxx:thread_yyy(所有人共享) |
多用户消息标记
# 文件: gateway/run.py, 第 7950 行
if _is_shared_multi_user and source.user_name:
message_text = f"[{source.user_name}] {message_text}"
agent 眼中看到的格式:
[at] 帮我改代码
[zhangsan] 我也看一下
[at] 这里报错了
所有消息按时间顺序混编成一个对话历史提供给 AIAgent。
五、多用户记忆限制分析
为什么无法成长性识别每个人风格
1. 记忆系统没有用户维度
memory 工具和 fact_store 是整个 Hermes 实例全局的。CLI、Telegram、Dashboard 存的记忆所有人都读得到。没有 user_id 作为记忆的 scope key。
关键源码路径:
agent/memory_provider.py—MemoryProvider抽象类:load()/save()没有 user_id 参数plugins/memory/holographic/__init__.py—HolographicMemoryProvider同样无用户维度tools/memory_tool.py—memory工具的 handler 不识别调用者身份
2. session key 不区分用户
同一个 topic 里,每个人的消息进同一个 session_key,同一个 AIAgent 实例。run_conversation() 加载的 conversation_history 是所有人的混编消息。
3. [at] 前缀只是纯文本标签,不是身份
agent 看到 [at] / [zhangsan] 知道是谁在说话,但没有:
- 独立的
user_profile存储 - 每个用户的记忆隔离空间
- 从对话中自动提取"某人的风格"并归类的机制
如果要实现需要改什么
# 需要新增(目前不存在)
class UserProfile:
user_id: str
display_name: str
communication_style: str # "简洁" / "详细" / "代码优先"
preferences: dict
memory_scoped_to_user: List[str]
# memory 工具需要加 user_id scope
memory(action="add", target="user", content="...", scope_user="at")
# run_conversation 需要按 user_id 加载独立 profile 到 system prompt
system_prompt += f"\n当前用户 {user_name} 的风格偏好: {profile}"
六、总结
| 场景 | 推荐使用 | 原因 |
|---|---|---|
| STDD 项目开发(高强度编码) | CLI ✅ | 视野全、响应快、无截断、实时看 tool call 输出 |
| 查看状态报告、轻度交互 | Telegram ✅ | 移动端方便、可中断、多人可看 |
| 多人协作同一 agent | Telegram topic ⚠️ | 共享会话但无个人风格记忆 |