Files
AgentsMeeting/gateway/docs/xxm 稳定性与观察者模式.md
T
hmo 1b2b935832 Initial: multi-agent XMPP communication system with dashboard
- Platform-based architecture (Windows/Linux/Mac)
- Agent instance registry (agents.yaml)
- Management dashboard with cross-platform monitoring
- xmpp_bot with HTTP bridge + health endpoints
- wechat_agent with WeChat-Hermes bridging
- Platform services: ProcessGuardian, HealthProbe, APIRouter, ChannelBridge
- Deployment: systemd (Linux) + PowerShell (Windows)
- Monitoring: SSH+ejabberdctl for cross-platform presence
2026-06-12 21:51:36 +08:00

230 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# xxm(小小莫)稳定性与观察者模式
> 最后更新:2026-06-03
---
## 架构概要
xxm`xxm@yoin.fun`)是一个 XMPP bot,通过 slixmpp 连接 ejabberd`xmpp.yoin.fun:3021`),作为"小小莫"在群聊 `coregroup@conference.yoin.fun` 中观察和响应消息。
```
XMPP 消息
xmpp_bot.py ← on_message / on_group_message
session_router.py → route() → 构建 prompt
chat_bridge.py → send_raw() → HTTP API 调用
volcengine API (deepseek-v4-flash)
```
---
## 2026-06-03 修复记录
### 一、HTTP 请求死锁(最严重的稳定性问题)
**问题**`urllib.request.urlopen(req, timeout=timeout)` 在 Windows 特定网络条件下,timeout 参数不触发,请求永远挂在 socket 上。桥接线程卡死 → bot 不回消息。
**修复**:弃用 `urllib.request`,改用 `requests` 库。
```python
# 旧(会挂死)
req = urllib.request.Request(url, data=data, headers=headers)
with urllib.request.urlopen(req, timeout=timeout) as resp:
body = json.loads(resp.read())
# 新(Connect=10s, Read=60s,分别起效)
resp = requests.post(url, json=payload, timeout=(10, timeout))
resp.raise_for_status()
body = resp.json()
```
**效果**
- Connect timeout 10sDNS / 连接层挂死不再发生
- Read timeout 60sAPI 响应慢也能兜底
- 异常精确分类:`Timeout` / `HTTPError` / `RequestException` 分别打不同日志
**涉及文件**`chat_bridge.py`
---
### 二、工具循环截断
**问题**`_MAX_TOOL_LOOPS = 5`,LLM 做多步排查(SSH 到服务器逐条查 nginx 配置)时很容易耗尽,耗尽后 `return None` → bot 告诉用户"模型无响应"。
**修复**
1. `_MAX_TOOL_LOOPS = 5 → 50`(足够做深度排查)
2. 50 轮耗尽后,再调一次**不带 tools 参数**的 API,强制模型用文字总结
```python
# 耗尽后的兜底
final_resp = session.post(final_url, json={"model": model, "messages": messages}, ...)
final_msg = final_resp.json()["choices"][0]["message"]["content"]
if final_msg.strip():
return final_msg.strip()
```
**涉及文件**`chat_bridge.py`
---
### 三、API Key 冷却与 retry-cache 绕过
**问题**opencode-go / opencode-go-new 两个 key 都在 403 冷却期(`code 1010`)。同时 opencode 的 retry-cache 机制(`#25803/#24462`)按 `(provider, model)` 缓存 429 错误,导致切换到另一个 provider 后仍然被旧 cache 截断。
**修复**
1. 创建 `api_proxy.py` 本地 HTTP 代理(`:8787`
2. 模型名重映射:`deepseek-v4-flash-safe``deepseek-v4-flash`(不同的模型名绕开 retry-cache key 碰撞)
3. 代理吞掉 429/5xx 错误码,自动重试最多 3 次(指数退避 1s/2s/4s
4. 代理配置为独立 provider`volcengine-proxy/deepseek-v4-flash-safe`
```json
{
"volcengine-proxy": {
"type": "openai",
"options": {
"baseURL": "http://localhost:8787",
"apiKey": "...",
"model": "deepseek-v4-flash-safe"
}
}
}
```
**涉及文件**`api_proxy.py``start_proxy.bat``~/.config/opencode/config.json`
---
### 四、观察者模式与 `__SILENT__` 协议
**问题**xxm 在群里应该只回应 @自己的消息,其他消息保持沉默。但 LLM 会说"我应该保持沉默"然后把这句话本身发出去(没有用 `__SILENT__` 前缀)。
**修复**(两层防护):
**第一层 — System Prompt 明确教学**`chat_bridge.py`):
```
=== 群聊沉默协议 ===
保持沉默的方法:在回复的最开头写 __SILENT__
系统检测到 __SILENT__ 就不会把消息发出去。
注意:不要直接把"我应该保持沉默"当回复发出去。
```
**第二层 — 自然语言兜底**`xmpp_bot.py`):
```python
_SILENCE_PATTERNS = [
"保持沉默", "不应[该]?回复", "没有.*@.*我",
"不是对[我我说]", "跟我无关", "我不用回复",
]
```
如果回复第一行匹配任何静默模式(即便没有 `__SILENT__` 前缀),直接 suppress。只检查第一行,避免误杀多行正常回复。
**涉及文件**`chat_bridge.py``xmpp_bot.py`
---
### 五、群消息合并与串行化
**问题**:同个群多条消息靠近到达时,每条都触发独立的 LLM 调用和工具循环,导致:
- 并发工具调用互相干扰(同时 SSH 到同一台服务器)
- 多条重复排查(浪费额度)
- log 混乱
**修复**3 秒 debounce + 房间级串行化
**三个状态 / 两个路径**
```
消息到达
├─ @xxm → 立即处理(绕过 batch
└─ 其他 → 进入 batch 系统
[BATCHING] 3s 窗口内可合并
│ Timer 到期
[PROCESSING] LLM 调用中
│ 新消息 → 进入 pending 队列
│ LLM 结束
[_batch_done] 检查 pending
├─ 有 pending → 立即发起下一批
└─ 无 pending → IDLE
```
**保证**:同一房间同一时刻最多一个 LLM 调用在处理。
**涉及文件**`xmpp_bot.py`
---
### 六、context log 保存(self-message
**问题**:LLM 看不到自己在群里说过什么,因为 bot 的 self-message 被直接丢弃了。每次都是"失忆"状态。
**修复**self-message 不走 LLM,但写入 bridge context log`_append_to_log("assistant", body)`),这样下次 LLM 调用时通过 `_read_recent_context()` 能看到自己说过的话。
**涉及文件**`xmpp_bot.py``on_group_message``nickname == bot_nick` 分支)
---
### 七、`part_` → `prt_` 前缀修复(2026-06-11
**问题**`chat_bridge.py``_append_to_session()` 生成 part ID 时用了 `part_` 前缀:
```python
part_id = "part_" + _uuid.uuid4().hex[:24] # BUG!
```
但 OpenCode 1.17+ 要求 part ID 必须用 `prt_` 前缀。这导致:
- 每次 xxm 写入消息到 session → 产生 `part_` 数据
- compaction 扫描到旧前缀 → schema 校验失败 → 死循环
- session 崩掉 → xxm 也卡住
**修复**`part_``prt_``chat_bridge.py:335`
**连带发现**`ses_xxm_xmpp` session 在代码里配了但 DB 里不存在,已手动创建。
### 八、`session_search` 工具说明
**用途**:让 xxm 能搜索其他 session 的历史对话。已内置为 function calling tool。
**定义位置**`chat_bridge.py``_TOOLS` 列表
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `session_id` | 当前 session | 指定要搜索的 session ID,空字符串则查自己 |
| `limit` | 20(最大100) | 返回最近多少条消息 |
**调用方式**LLM 通过 function calling 调用 `session_search`,不需要 xxm 写代码。
**注意**archived session 仍可搜索(`extract_session_context` 直接从 message 表读,不依赖 session 状态)。
---
## 关键配置
| 参数 | 值 | 说明 |
|------|-----|------|
| `_MAX_TOOL_LOOPS` | 30 | 工具循环上限(超限后 clean final force,不泄漏 XML |
| `DEFAULT_TIMEOUT` | 60s | 每次 API 调用 read timeout |
| `LOCK_DURATION` | 300s | 锁定成功 provider(避免频繁切换) |
| `FAILED_BACKOFF` | 1800s | 失败 provider 冷却 |
| `_BATCH_WINDOW` | 3.0s | 群消息合并窗口 |
| api_proxy 重试 | 3 次 | 指数退避 1s/2s/4s |
## Provider 优先级
```
volcengine (deepseek-v4-flash) # 主用,火山免费额度
→ opencode-go-new (deepseek-v4-flash) # 备用,有订阅但冷却中
→ opencode-go (deepseek-v4-flash) # 备用,冷却中
```
## 日志
| 日志文件 | 路径 | 用途 |
|---------|------|------|
| `logs/xmpp_bot.log` | `projects/.../logs/` | XMPP 连接、消息收发、batch 状态 |
| `logs/bridge.log` | `projects/.../logs/` | LLM API 调用、耗时、工具调用 |
| `logs/api_proxy.log` | `projects/.../logs/` | 代理请求、错误吞没、重试 |