TL;DR :Honcho 里「用户画像为空」往往不是故障,而是查错了视角。peer card 按「观察者→被观察者」二元组存储:Boss 观察自己返回
null,但助手观察 Boss 已有 2 条画像。日常对话只触发 deriver 生成结论(conclusions),不会自动写 peer card------只有 omni dream(每 50 条 explicit 文档 + 8 小时冷却)或显式 API 调用才会更新卡片。本文基于真实部署(Jetson 自托管 + PostgreSQL),从存储模型、触发链路、结构校验到写入实操完整拆解,所有结论均有源码与实测双重证据。

一、现象:1956 条结论在手,画像却显示「空」
我们在生产环境跑 Honcho 已经一周。某天排查发现一个诡异现象:用户明明每天都在对话、结论库已经积累了 1956 条(conclusions),但查询用户画像却返回空:
bash
# 查询用户(Boss)的画像卡片
curl http://<honcho-server>:8001/v3/workspaces/<ws>/peers/Boss/card
# 返回:{"peer_card": null}
「结论有 1956 条,画像却是 null」------第一反应是系统坏了:是不是 deriver 没跑?是不是结论没落库?是不是权限问题?排查了半天,最后发现画像根本不是空的,只是查错了方向:
bash
# 用助手(Friday)视角查询它对 Boss 的画像
curl "http://<honcho-server>:8001/v3/workspaces/<ws>/peers/Friday/card?target=Boss"
# 返回:{"peer_card": ["Boss 是电力行业人工智能专业(边缘模型部署/CV视觉检测/语音全链路/大模型应用/Agent编排/AI内容生产),做 OPC(科技×C端消费者)变现", "Boss 品牌=含光(hanguang),AI 内容 + 私域变现,抖音不靠播放分成靠私域收钱"]}
同一个用户,两种视角,两种结果。这不是玄学,是 Honcho 的数据模型设计------peer card 从来就不是「用户的画像」,而是「观察者对用户的画像」。
二、存储模型:peer card 是「观察者视角」的二元组
核心发现:卡片存在观察者身上,不是被观察者身上
翻开 Honcho 源码 src/crud/peer_card.py,存储逻辑一目了然。peer card 不是用户表里的一个字段,而是存在 observer(观察者)peer 的 internal_metadata 里:
python
# src/crud/peer_card.py:96-108
stmt = (
update(models.Peer)
.where(models.Peer.workspace_name == workspace_name)
.where(models.Peer.name == observer) # ← 注意:写的是 observer!
.values(
internal_metadata=models.Peer.internal_metadata.op("||")(
{
construct_peer_card_label(observer=observer, observed=observed): peer_card
}
)
)
)
而卡片的 key 由 construct_peer_card_label 决定:
python
# src/crud/peer_card.py:123-126
def construct_peer_card_label(*, observer: str, observed: str) -> str:
if observer == observed:
return "peer_card"
return f"{observed}_peer_card"
这意味着两件事:
| 查询方式 | observer | observed | 存储 key | 结果 |
|---|---|---|---|---|
peers/Boss/card(不带 target) |
Boss | Boss | peer_card |
null(没人写过「Boss 看 Boss」) |
peers/Friday/card?target=Boss |
Friday | Boss | Boss_peer_card |
2 条画像 |
peers/Boss/card?target=Friday |
Boss | Friday | Friday_peer_card |
null(没人写过「Boss 看 Friday」) |
所以「画像为空」的真实含义是:没有人以这个视角写过卡片。Honcho 的哲学是「谁观察、谁记录」------每个 peer 都维护自己对其他 peer 的认知,而不是存在一个全局的用户档案表。这个设计的好处是天然支持多 Agent 场景:两个不同的 Agent 对同一个用户可以有完全不同的画像(比如客服 Agent 记录「用户偏好退款」,销售 Agent 记录「用户预算充足」),互不污染。
为什么「用户看自己」是 null?
顺着这个模型看,「用户自观察为空」是完全正常的设计:用户不会主动对自己形成画像------画像的形成主体是 AI 助手(Agent peer),它观察用户(User peer)并记录。所以正确的查询姿势永远是:
bash
# 查询「助手 peer 对用户 peer 的画像」
curl "http://<host>:8001/v3/workspaces/{ws}/peers/{agent_peer}/card?target={user_peer}"
而不是:
bash
# ❌ 错误姿势:查询用户对自己的画像(几乎永远是 null)
curl "http://<host>:8001/v3/workspaces/{ws}/peers/{user_peer}/card"
三、触发链路:peer card 到底什么时候才会被写?
确认了存储模型,下一个问题是:这 2 条画像到底是谁写的? 如果日常对话不写卡片,那 Honcho 凭什么说自己是「持续演化的用户画像」?
3.1 deriver(日常推理):只写结论,不写卡片
先看最频繁触发的组件------deriver。每次对话后它会异步分析消息、生成关于用户的结论(conclusions)。我们实测 4 条对话注入后 30 秒自动生成 11 条结论。但翻遍 deriver 的代码,它只负责生成 observation/conclusion,从不调用 set_peer_card:
bash
# 全库搜索 deriver 是否写 peer card
grep -rn "peer_card\|set_peer_card" src/deriver/ # 无任何输出
这解释了第一部分的现象:conclusions 有 1956 条 ≠ 画像有 1956 条。结论是「原材料」,peer card 是「成品」,中间还隔着一道「dream」工序。
3.2 omni dream(合并推理):唯一自动写卡片的路径
Honcho 的 dreamer 模块才是卡片的生产者。在 src/utils/agent_tools.py 里,deduction specialist(dreamer 第一阶段)的工具集明确包含 update_peer_card:
python
# src/utils/agent_tools.py:826-840
DEDUCTION_SPECIALIST_TOOLS = [
TOOLS["get_recent_observations"],
TOOLS["search_memory"],
TOOLS["search_messages"],
TOOLS["create_observations_deductive"],
TOOLS["delete_observations"],
TOOLS["update_peer_card"], # ← dream 阶段会写卡片
]
但 dream 不是每次对话都跑,它有两个硬性门槛(src/dreamer/dream_scheduler.py + src/config.py):
| 门槛 | 默认值 | 含义 |
|---|---|---|
DOCUMENT_THRESHOLD |
50 | explicit 级文档新增满 50 条才触发一次 dream |
MIN_HOURS_BETWEEN_DREAMS |
8 小时 | 两次 dream 之间至少间隔 8 小时 |
IDLE_TIMEOUT_MINUTES |
60 | 到阈值后延迟 60 分钟空闲期再跑(避免高峰抢资源) |
也就是说:正常对话永远不会立刻更新画像 ------你至少要积累 50 条 explicit 文档、再等 8 小时冷却、再等 60 分钟空闲,dream 才会跑一次,deduction specialist 才会根据结论重写卡片。这也是很多 Honcho 初用者「对话了几天画像还是 null」的根因:阈值没到,或者只看了自观察视角。
3.3 card_refresh dream:只在 scope 变更时触发
除了 omni dream,Honcho 还有一种 card_refresh 类型(src/dreamer/orchestrator.py:314 的 run_card_refresh_dream),它只调用 get_recent_observations、search_memory、update_peer_card 三个工具,专门做卡片重建。但它的触发条件更冷门------session 加入或移出 scope(会话分组)时 (src/deriver/scope_backfill.py),日常使用基本遇不到。
3.4 三种写卡路径总结
| 路径 | 触发条件 | 频率 | 写卡片? |
|---|---|---|---|
| deriver(日常) | 每条消息 | 每次对话 | ❌ 只写结论 |
| omni dream | ≥50 explicit + ≥8h 冷却 + 60min 空闲 | 约几天一次 | ✅ deduction 阶段 |
| card_refresh dream | scope 变更(session 加入/移出分组) | 极少 | ✅ 只重建卡片 |
| 手动 API | 你主动调用 | 随时 | ✅ 立即写入 |
3.5 从消息到画像的完整时序(含真实时间线)
用一次真实实验串起整个链路。我们注入 4 条双 peer 对话后,按时间顺序观察:
| 时间点 | 发生了什么 | 画像状态 |
|---|---|---|
| T+0s | 消息 POST,API 落库 | conclusions 0 条,card null |
| T+30s | deriver 完成推理,生成 11 条 explicit 结论 | conclusions 11 条,card 仍 null |
| T+30s 后 | 结论向量化(bge-m3)并持久化 | 语义搜索可命中结论 |
| T+8h(假设) | 若期间 explicit 累计 ≥50 条,omni dream 启动 | deduction 阶段可能重写卡片 |
| T+8h 后 | dream 完成 | card 更新为最新画像快照 |
关键认知:从「对话」到「画像」之间永远隔着 deriver(结论)和 dream(卡片)两道工序。你看到的「画像空」大概率是卡在了第二道工序------要么没到阈值、要么还在冷却、要么根本没开 dream。而结论(第一道工序)其实是实时完成的------所以用 conclusions 接口验证「系统有没有在学」是可靠的,用 card 验证则可能产生误判。
3.6 官方与社区都在回答同一个问题
「为什么我的 Honcho 画像空」不是我们一家遇到的问题。GitHub 上 plastic-labs/honcho 仓库的 Issue #494 (仓库 6.8k star)标题就是《Self-hosted/local Honcho ingests messages but derived memory does not appear automatically; peer card/context/search remain empty》------自托管用户报告消息成功写入、deriver 也手动跑过,但 peer card / profile 表面仍然为空。官方回复指向的根因之一就是 deriver 是否在自动运行(自托管时 deriver 进程没启动就不会有任何派生记忆)。
更有力的佐证来自 Honcho 3.0 官方发布博客 :官方明确说明 peer cards 的生成「moved to the dreaming system」------由于 dream 是间歇运行的,「这些对象不会出现在尚未经历过 dream 的 session/peer 上」(these objects won't be available for sessions and peers that haven't undergone dreaming)。也就是说,「画像空」是设计使然,不是 bug:官方自己都在文档里承认没有 dream 就没有 card。官方 Changelog 里甚至直接写着「peer cards no longer created/updated in deriver process」------deriver 不写卡片是官方确认的架构决策,不是我们部署错了。
这也意味着我们的部署行为完全正常:deriver 在跑(1956 条结论),但 dream 因为阈值/冷却还没产出卡片,而 Friday 视角的 2 条是显式写入的结果。如果你也遇到这个问题,先对照本文第六节的排查清单,大概率能找到答案。
四、结构校验:不是随便写,四条前缀 + 两道护栏
既然可以手动写卡片,那「写什么」有没有限制?有,而且很严格。update_peer_card 工具和 PUT /card API 都走同一套校验(src/utils/agent_tools.py:58):
4.1 四条允许前缀
卡片每条必须以这四个前缀之一开头,必须带一个空格,否则整条被拒:
| 前缀 | 含义 | 示例 |
|---|---|---|
IDENTITY: |
身份事实 | IDENTITY: Boss 是电力行业 AI 工程师 |
ATTRIBUTE: |
属性特征 | ATTRIBUTE: 偏好结论先行的沟通风格 |
RELATIONSHIP: |
关系 | RELATIONSHIP: 与含光品牌绑定,做内容变现 |
INSTRUCTION: |
对 Agent 的指令 | INSTRUCTION: 回复前先给结论再展开 |
python
# src/utils/agent_tools.py:47-52
PEER_CARD_ALLOWED_PREFIXES = (
"IDENTITY:", "ATTRIBUTE:", "RELATIONSHIP:", "INSTRUCTION:",
)
注意:旧版本卡片的条目(如 Name: Alice、TRAIT: Analytical)不会自动兼容------源码注释明确说明这些不带前缀的旧条目会被静默拒绝,需要 Agent 在重写时主动迁移成新格式。
4.2 两道护栏
| 护栏 | 上限 | 违反后果 |
|---|---|---|
MAX_PEER_CARD_FACTS |
40 条 | 超出截断到 40 条 |
MAX_PEER_CARD_ENTRY_LENGTH |
200 字符/条 | 超出拒绝该条 |
还有一层保护:空内容不清卡 (agent_tools.py:1690-1700)。如果传进来的内容全部校验失败或为空,会保留现有卡片,而不是把卡片清空------防止 Agent 一次错误的工具调用把积累的画像抹掉。实测反馈机制也很友好:被拒的条目会回传给模型(最多 3 条样例),让 Agent 可以自我纠错后重试。
五、实操:三种姿势把画像写进去
姿势一:手动 API 写入(最快,适合初始化)
bash
curl -X PUT "http://<host>:8001/v3/workspaces/<ws>/peers/Friday/card?target=Boss" \
-H "Content-Type: application/json" \
-d '{"peer_card": [
"IDENTITY: Boss 是电力行业人工智能专业,做 OPC(科技×C端消费者)变现",
"ATTRIBUTE: 偏好结论先行的沟通风格",
"INSTRUCTION: 回复前先给结论,再展开证据"
]}'
写入后立即生效,无需等待 dream。适合在首次部署时把已知的用户画像一次性初始化进去。
姿势二:靠 omni dream 自动演化(推荐长期用)
什么都不用做,保持对话积累即可。但要注意两点:
- 确认 dream 开启 :
config.toml里[dream] ENABLED = true(默认开启),[peer_card] ENABLED = true - 验证是否触发 :查 collection 的
internal_metadata.dream.last_dream_at,或观察 deriver 日志里是否有Starting dream记录
如果对话量大但 dream 一直不跑,检查是否反复被 MIN_HOURS_BETWEEN_DREAMS 拦住(每次刚过阈值就 dream,8 小时内不会重跑)。
姿势三:Agent 工具写入(给 AI 自己用)
Honcho 的 agent 工具集里内置了 update_peer_card,Agent 在对话过程中发现用户的稳定偏好时可以直接调用(比如用户连续三次说「不要列选项」→ Agent 写入 ATTRIBUTE: 不喜欢选择题式回复)。结构校验失败时工具会返回被拒样例,Agent 可以修正后重试。
六、排查清单:画像为空时按这个顺序查
| # | 检查项 | 方法 | 判定 |
|---|---|---|---|
| 1 | 是否查错视角 | 用 ?target= 指定被观察者 |
最常见根因,Agent→User 视角才有内容 |
| 2 | dream 是否开启 | config.toml [dream] ENABLED |
false 则永远不自动写 |
| 3 | 是否到阈值 | 查 explicit 文档数(DOCUMENT_THRESHOLD=50) |
没到 50 条不触发 |
| 4 | 是否被 8h 冷却拦截 | 查 last_dream_at |
刚 dream 完 8h 内不重跑 |
| 5 | 校验是否通过 | 查 deriver 日志 rejected 记录 | 前缀/长度/上限不满足会被拒 |
| 6 | 结论有没有在积累 | 查 conclusions 总数 | conclusions 也为 0 才是真没在学 |
七、与 Hindsight 的对照:两种记忆哲学
如果你在同一个 Agent 上同时用 Hindsight 和 Honcho,会发现它们对「画像」的理解完全不同:
| 维度 | Hindsight(事实记忆库) | Honcho(人格画像) |
|---|---|---|
| 存储单位 | 全局事实(fact),打 topic/stage 标签 | 观察者→被观察者的二元组卡片 |
| 写入时机 | auto_retain 每 20 轮自动存 | 结论实时生成,卡片需 dream/显式写 |
| 查询视角 | 无视角概念,全局可查 | 必须指定 observer + observed |
| 更新方式 | 事实合并(consolidation) | 卡片整体重写(40 条上限) |
理解这个差异,就能避免很多「记忆系统不工作」的误判:Hindsight 查不到是「没存进去」,Honcho 查不到是「没写卡片 / 查错视角」------两码事。
八、写在最后
Honcho 的 peer card 是一个被误解最多的组件。它叫「卡片」,但本质是每个 Agent 对其他实体的认知快照,存储在对应用户(观察者)身上,由 dream 机制低频重写。下次你查画像返回 null,先别急着怀疑系统坏了------检查一下:是不是查成了「用户看自己」?是不是 explicit 文档还没到 50 条?是不是 dream 刚跑完还在 8 小时冷却期?
排查顺序永远先查视角,再查触发,最后才查故障。
九、常见误区 FAQ
Q1:conclusions 有 1956 条,为什么画像还是空的? A:conclusions 是 deriver 实时生成的「原材料」,peer card 是 dream 低频重写的「成品」。原材料多 ≠ 成品有------中间还隔着 50 条阈值 + 8 小时冷却。另外要确认查询视角:peers/User/card 查的是用户看自己(几乎永远 null),必须用 peers/Agent/card?target=User。
Q2:我已经等了一周,画像还是 null,是不是系统坏了? A:先做三步排查:① 查 config.toml 的 [dream] ENABLED 和 [peer_card] ENABLED 是否 true;② 查 explicit 文档数是否到 50(conclusions/list 里 explicit 级的数量);③ 查 deriver 日志有没有 Starting dream / rejected 记录。三步都正常但画像仍空,多半是视角问题(见 Q1)。
Q3:能不能让画像立即更新? A:能。手动 PUT /v3/workspaces/{ws}/peers/{agent}/card?target={user} 立即写入,适合初始化或修正。日常演化则靠 dream 自动跑,但要有耐心------它设计成低频操作,不是实时的。
Q4:卡片写多了会不会爆? A:不会。MAX_PEER_CARD_FACTS=40 上限,超出截断;每条 MAX_PEER_CARD_ENTRY_LENGTH=200 字符,超出拒绝。旧条目不带前缀会被静默丢弃,所以重写时要把想保留的条目重新按新格式输出。
Q5:多 Agent 场景下画像会互相污染吗? A:不会。卡片按 (observer, observed) 二元组隔离------Agent A 对用户的画像存在 A 的 internal_metadata 里,Agent B 看不到也改不了 A 的画像。这是 Honcho 相比全局用户表方案的核心优势。
Q6:怎么确认系统「真的在学」? A:看 conclusions 总数是否增长(POST /v3/workspaces/{ws}/conclusions/list),这是 deriver 在工作的直接证据。画像卡片只是「学到的东西」的精选快照,不是学习行为本身。
原始出处 :本文全部源码结论来自 Honcho 开源仓库(plastic-labs/honcho)实际部署版本
src/crud/peer_card.py、src/utils/agent_tools.py、src/dreamer/dream_scheduler.py、src/dreamer/orchestrator.py,实测数据来自自托管实例(PostgreSQL + 本地 embedding)。