第 7 章 多模态记忆摄入 智能体的输入不止聊天文字。memU 内置对话、文档、图片、视频、音频五条预处理管道——本章讲清每种模态的行为、配置要点与典型场景。 7.1 多模态在记忆中的价值 所有模态最终汇聚为 文本描述(caption),再进入同一套提取流水线。检索侧无需区分模态——除非你要按 过滤。 7.2 五种 modality 对照 modality | 输入示例 | 预处理 | 典型 memorytype | 聊天 JSON、消息列表 | 解析 role/content | profile, event | Markdown、PDF 文本、日志 | 纯文本提取 | knowledge, tool | PNG、JPG、截图 | Vision 模型 caption |
智能体的输入不止聊天文字。memU 内置对话、文档、图片、视频、音频五条预处理管道——本章讲清每种模态的行为、配置要点与典型场景。
┌─────────────┐ │ 用户世界 │ │ 文本/图/音/视 │ └──────┬──────┘ │ memorize(modality=...) ▼ ┌─────────────┐ │ caption │ ← 统一的文本 trunk └──────┬──────┘ │ extract_items ▼ ┌─────────────┐ │ MemoryItem │ └─────────────┘
所有模态最终汇聚为 文本描述(caption),再进入同一套提取流水线。检索侧无需区分模态——除非你要按 Resource.modality 过滤。
| modality | 输入示例 | 预处理 | 典型 memory_type |
|---|---|---|---|
conversation |
聊天 JSON、消息列表 | 解析 role/content | profile, event |
document |
Markdown、PDF 文本、日志 | 纯文本提取 | knowledge, tool |
image |
PNG、JPG、截图 | Vision 模型 caption | knowledge, event |
video |
MP4、会议录像 | 抽帧 + 描述 | event, knowledge |
audio |
MP3、WAV、M4A | Transcribe 转写 | event, knowledge |
conversation 来源通常是 JSON 数组,每条含 role 与 content:
[ {"role": "user", "content": "我下周要去上海出差。"}, {"role": "assistant", "content": "需要我帮你查酒店吗?"}, {"role": "user", "content": "Prefer 靠近虹桥,安静一点的。"} ]
await service.memorize( resource_url="https://storage.example.com/session-42.json", modality="conversation", user={"user_id": "alice"}, )
await service.memorize( resource_url="https://wiki.example.com/architecture-overview", modality="document", ) await service.memorize( resource_url="https://logs.example.com/agent-trace.log", modality="document", user={"agent_id": "deploy_bot"}, )
把 PR 描述、设计文档、失败 build 日志持续 memorize,检索时问:
「这个模块应该怎么分层?」「上次 deploy 失败原因?」
Vision 模型(vision Profile 或 default 中的视觉模型)生成自然语言 caption。
await service.memorize( resource_url="https://cdn.example.com/ui-mockup-v3.png", modality="image", )
service = MemoryService( llm_profiles={ "default": {"api_key": "...", "chat_model": "gpt-4o"}, "vision": {"api_key": "...", "chat_model": "gpt-4o"}, }, )
对视频抽帧或片段,用 Vision 生成内容摘要,写入 Resource.caption。
await service.memorize( resource_url="https://meetings.example.com/sprint-review.mp4", modality="video", user={"user_id": "team_alpha"}, )
Transcribe 模型(Whisper 类)转写为文本。
await service.memorize( resource_url="https://voice.example.com/standup-20250623.m4a", modality="audio", )
llm_profiles={ "default": {"api_key": "...", "chat_model": "gpt-4o-mini"}, "transcribe": {"api_key": "...", "chat_model": "whisper-1"}, }
写入多种模态后,retrieve 不区分 modality:
context = await service.retrieve( queries=[{ "role": "user", "content": {"text": "关于 onboarding 我们有哪些材料?"}, }], where={"user_id": "team_alpha"}, )
可能同时召回:
若需只查某模态,可在应用层过滤 resources[].modality。
async def build_project_memory(service, project_id): sources = [ ("https://docs.example.com/prd.md", "document"), ("https://design.example.com/wireframe.png", "image"), ("https://meetings.example.com/kickoff.mp4", "video"), ("https://chat.example.com/slack-export.json", "conversation"), ] scope = {"user_id": project_id} total_items = 0 for url, modality in sources: result = await service.memorize( resource_url=url, modality=modality, user=scope, ) total_items += len(result["items"]) print(f"[{modality}] +{len(result['items'])} items") return total_items
通过 OpenRouter 可在一个 Profile 内切换视觉模型:
MemoryService( llm_profiles={ "default": { "provider": "openrouter", "client_backend": "httpx", "base_url": "https://openrouter.ai", "api_key": "your_key", "chat_model": "openai/gpt-4o", "embed_model": "openai/text-embedding-3-small", }, }, )
Vision、Embedding、Chat 均走 OpenRouter 统一账单,适合快速试验。
| 模态 | 主要成本 | 降本建议 |
|---|---|---|
| conversation | Chat 提取 | 用小模型提取,大模型只负责对话 |
| document | Chat 提取 | 预清洗 HTML / 去重 |
| image | Vision caption | 压缩分辨率;批量离线处理 |
| video | Vision × 帧数 | 先转 audio 再 transcribe |
| audio | Transcribe 时长 | 切片;跳过静音段 |
「多模态摄入」示例演示 conversation / document / 工具日志三种模态的写入,并统计各来源的 memory_type 分布。下面是核心脚本:
"""多模态记忆摄入完整示例。 演示 conversation / document 两种模态的写入,统计各来源提取出的 memory_type 分布,验证不同模态会沉淀出不同类型的记忆项。 依赖:pip install memu-py (需配置 LLM API Key) """ import asyncio import json import tempfile import os from memu import MemoryService CONVERSATION = [ {"role": "user", "content": "我在做一个电商推荐项目,下个月要上线 A/B 测试。"}, {"role": "assistant", "content": "了解,需要我帮忙规划测试方案吗?"}, {"role": "user", "content": "对,我们用多臂老虎机策略,避免流量浪费。"}, {"role": "assistant", "content": "好的,我会记录这个决策。"}, ] DOCUMENT = """\ 项目周报 进展:推荐召回模块完成离线评估,recall@50 达到 0.72 风险:特征中心延迟上涨,需排查 Redis 热点 key 下周:联调在线 serving,配置监控告警 """ def write_temp(obj, prefix, is_json=True): p = os.path.join(tempfile.gettempdir(), f"{prefix}_{os.getpid()}" + (".json" if is_json else ".txt")) with open(p, "w", encoding="utf-8") as f: f.write(json.dumps(obj, ensure_ascii=False) if is_json else obj) return p def print_type_distribution(result, tag): types = {} for it in result.get("items", []): t = it.get("memory_type", "?") types[t] = types.get(t, 0) + 1 print(f" {tag}: {types}") async def main(): service = MemoryService( llm_profiles={"default": {"api_key": "your_api_key", "chat_model": "gpt-4o-mini"}}, database_config={"metadata_store": {"provider": "inmemory"}}, retrieve_config={"method": "rag"}, user_config={"model": {"user_id": str, "agent_id": str}}, ) user = {"user_id": "ops_team", "agent_id": "devops_bot"} # ── conversation 模态 ── conv_url = write_temp(CONVERSATION, "conv") r1 = await service.memorize(resource_url=conv_url, modality="conversation", user=user) print_type_distribution(r1, "conversation") # ── document 模态(周报)── doc_url = write_temp(DOCUMENT, "doc", is_json=False) r2 = await service.memorize(resource_url=doc_url, modality="document", user=user) print_type_distribution(r2, "document(周报)") if __name__ == "__main__": asyncio.run(main())
预期看到:conversation 多提取 profile / event,document 多提取 knowledge / behavior。图片 / 音频模态的调用方式相同,只需把 modality 改为 image / audio 并配置对应 Profile。
💡 工具日志那条特别值得关注——它是「编码智能体从执行经验中学习」的原型:把 agent 的 tool 轨迹摄入,后续 retrieve 就能召回「编辑 YAML 前先全库搜索 key」这类可复用经验。
下一章:第 8 章 — 存储后端与 LLM 路由。
回顾:第 5 章 preprocess 步骤;第 4 章 llm_profiles。