画像空不是故障:Honcho peer card 观察者视角源码拆解——1956 条结论为何换不来一张卡

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:314run_card_refresh_dream),它只调用 get_recent_observationssearch_memoryupdate_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: AliceTRAIT: 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 自动演化(推荐长期用)

什么都不用做,保持对话积累即可。但要注意两点:

  1. 确认 dream 开启config.toml[dream] ENABLED = true(默认开启),[peer_card] ENABLED = true
  2. 验证是否触发 :查 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.pysrc/utils/agent_tools.pysrc/dreamer/dream_scheduler.pysrc/dreamer/orchestrator.py,实测数据来自自托管实例(PostgreSQL + 本地 embedding)。

相关推荐
starrysky8107 天前
Hindsight 记忆数据增删改实操:70 个 API 端点的 CRUD 地图
angular.js
starrysky8107 天前
从 472ms 到 150ms:Hindsight 的 SQL 模板为何能超越 Text2SQL
angular.js
starrysky81014 天前
sqlite3.OperationalError: database is locked 为什么 timeout=10 秒没生效?SQLite 锁升级死锁路径完整排障
angular.js
starrysky81014 天前
SMBus is busy, can't use it! + task blocked 120s - i2c-i801 错误路径释放未获取资源致 hung task(CVE-2026-64205)
angular.js
starrysky8101 个月前
kubectl logs 看不了日志?fsnotify 报 too many open files 的真相是 inotify 实例耗尽
angular.js
shmily麻瓜小菜鸡1 个月前
vue3里“devDependencies 是开发环境依赖,dependencies 是生产环境依赖”
前端·javascript·vue.js·vscode·vue·angular.js
starrysky8101 个月前
RuntimeError: dictionary changed size during iteration——多线程 Pydantic cached_property 竞态条件
angular.js
starrysky8101 个月前
Agent 安全 #06:Agent 灰度发布与回滚 —— 上线新 Prompt 炸了生产,你敢回滚吗?
angular.js
starrysky8101 个月前
CPU 70% idle 但 load average 84?D 状态进程不可中断睡眠的深度排查
angular.js