数据截至 (上游 commit 538b61f24529)
拦截与自动捕获:register 如何劫持 LLM 调用
30 秒导读: 你只写一行
memori.llm.register(client),之后照常调用 OpenAI / Anthropic / Gemini。Memori 在背后把你的client.chat.completions.create悄悄换成了自己的包装方法——每一次调用它都会:请求发出前把历史与召回的记忆注进 prompt,调用真实的官方方法,拿到回复后再攒出一份"待处理载荷"交给记忆管线。整个过程你的业务代码一个字都不用改。这就是所谓的零改代码捕获。
本章只回答一个问题:Memori 怎么在不改你代码的前提下,把一次 LLM 调用变成一份待处理载荷?
- 载荷之后如何被炼成记忆 → 见 02-augmentation.md。
- 注进 prompt 的那些"召回记忆"是从哪检索来的 → 见 03-recall.md。本章只讲注入发生的时机和位置,不讲检索逻辑。
- 顶层全景 → 见 index.md。
1. 这是什么(零基础也能懂)
一句话定义
Memori 的捕获层是一套 monkey patch(猴子补丁,运行时把一个对象的方法替换成你自己的实现) 机制:它把官方 SDK 客户端上"发起对话"的那个方法,替换成一个功能相同、但前后多做了两件事的包装方法。
它要解决的问题
你想给一个已经写好的 AI 应用加"长期记忆",但不想重写调用代码。传统做法是自己在每次调用前后插桩:调用前查数据库拼 prompt、调用后把对话存起来。这既繁琐又容易漏。
Memori 的思路是:你只注册一次,之后所有调用自动被拦截。
用起来什么样
from openai import OpenAI
from memori import Memori
client = OpenAI()
memori = Memori(...) # 建立记忆实例
memori.llm.register(client) # ← 关键的一行:劫持这个 client
# 之后照常用,一字不改。但每次 create 都被 Memori 接管了
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "我住在东京"}],
)
注册那一行返回后,client.chat.completions.create 已经不是 OpenAI 原来的方法了——它指向了 Memori 的包装器。你调用它的姿势不变,但行为多了"记忆注入"和"对话捕获"。
一句话直觉
把它想成给水管中间接了个透明的三通阀:水(你的请求/回复)照样流过,但阀门每次都能取一瓢(捕获对话)、也能掺一点(注入记忆),而上下游的水管(你的代码、官方 SDK)都感觉不到它的存在。
2. 顶层全景(捕获层怎么转)
捕获层内部分三步走,对应三个模块:
| 阶段 | 干什么 | 核心文件 |
|---|---|---|
| ① 注册 | 认出你传进来的是哪家 client,把它的方法替换成包装器 | memori/llm/_registry.py、memori/llm/clients/direct.py |
| ② 拦截 | 每次调用时,请求前注入、调用真方法、按返回类型分流 | memori/llm/invoke/invoke.py |
| ③ 捕获 | 响应回来后组装成标准载荷,交给记忆管线 | memori/llm/pipelines/post_invoke.py |
主线走一遍(从左到右,一次 LLM 调用的生命周期):
┌───────────── 注册期(register,只发生一次)─────────────┐
你的代码 ─register(client)─▶ 匹配器认出 provider ─▶ 把 create 换成 Invoke.invoke
└───────────────────────────────────────────────────────┘
┌───────────── 调用期(每次 create 都发生)──────────────┐
① 注入召回记忆(recall)
你调 create ─▶ Invoke.invoke ─▶ ② 注入历史对话(conversation)
③ 调用真实的官方 create ← self._method
④ 看返回类型分流
├─ 同步对象 ─▶ 立刻捕获
└─ 流式迭代器 ─▶ 边迭代边攒,结束时捕获
⑤ handle_post_response ─▶ 组装载荷 ─▶ 交给记忆管线(→02)
└───────────────────────────────────────────────────────┘
怎么读这张图: 上半区是"注册"——一次性动作,把方法换掉;下半区是"调用"——之后每次
create都会跑一遍 ①→⑤。①②是本章的"注入时机",⑤是本章的终点"载荷"。
3. 注册:register 如何认出 client 并替换方法
3.1 它要解决的小问题
register(client) 收到的可能是 OpenAI、Anthropic、Google、xAI、LiteLLM 里的任意一个,每家 SDK 的方法路径都不一样(OpenAI 是 client.chat.completions.create,Anthropic 是 client.messages.create……)。第一步得先认出这是谁,再决定去 patch 哪个方法。
3.2 思路:matcher 注册表 + 按注册顺序匹配
Memori 用一张注册表解决"认人"的问题。每个 provider 的包装类头上挂一个装饰器,把"一个判断函数(matcher)"和"这个类"配成一对,存进 Registry._clients 字典。
真源码 memori/llm/_registry.py:33 的 Registry.register_client:
@classmethod
def register_client(cls, matcher): # matcher: 一个 (client) -> bool 的判断函数
def decorator(client_class):
cls._clients[matcher] = client_class # 把"判断函数 → 包装类"存进注册表
return client_class
return decorator
这段的意思:它是个装饰器工厂,把 matcher(判断某个 client 是不是本家)和对应的包装类登记进 _clients。
包装类就这样自我登记。看 memori/llm/clients/direct.py:24 和 :155:
@Registry.register_client(client_is_anthropic) # 用 client_is_anthropic 判断
class Anthropic(BaseClient): ...
@Registry.register_client(client_is_openai) # 用 client_is_openai 判断
class OpenAi(BaseClient): ...
matcher 本身很朴素——就是看 client 的模块名前缀。memori/llm/_utils.py:35 的 client_is_anthropic:
def client_is_anthropic(client) -> bool:
return _client_module(client).startswith("anthropic") # 看 __module__ 是不是以 anthropic 开头
3.3 匹配:遍历注册表,第一个命中就用
注册进来的 client 交给 Registry.client() 解析。memori/llm/_registry.py:51:
def client(self, client_obj, config):
for matcher, client_class in self._clients.items(): # 按注册顺序遍历
if matcher(client_obj): # 第一个返回 True 的
return client_class(config) # 就实例化它的包装类
...
raise UnsupportedLLMProviderError(provider) # 都不匹配就报错
关键细节: 匹配是按注册顺序的线性扫描,谁先命中用谁(注释在 _registry.py:24 明确写了 "selected by matcher registration order")。所以 matcher 之间要互斥、别重叠,否则顺序会决定归属。
register_llm(_registry.py:77)是对外总入口:它先分流(直连 client 走 Registry().client(...);Agno / LangChain 这类框架走命名参数 openai_chat= / chatopenai=,委托给 memori.agno.register / memori.langchain.register),然后调 client_handler.register(client) 完成真正的替换(_registry.py:176-178)。
支持的 provider / adapter 一览:
| 家族 | client matcher(_utils.py) | 直连包装类(clients/direct.py) | 响应 adapter(adapters/*/_adapter.py) |
|---|---|---|---|
| OpenAI | client_is_openai | OpenAi | openai/_adapter.py |
| Anthropic | client_is_anthropic | Anthropic | anthropic/_adapter.py |
client_is_google | Google | google/_adapter.py | |
| xAI | client_is_xai | XAi | xai/_adapter.py |
| Bedrock | (经 LangChain 命名参数) | — | bedrock/_adapter.py |
| LiteLLM | client_is_litellm | LiteLLM | 复用 openai adapter |
| PydanticAi | client_is_pydantic_ai | PydanticAi | 复用 openai 系 |
adapter 是"怎么把这家的响应结构解析成统一 messages"的解析器,和 client 包装类一一对应但各管一段。adapter 也用同一套注册表(
register_adapter,_registry.py:41;在memori/llm/__init__.py:13于 import 时统一注册)。本章只讲到它"存在且按 provider 选中",解析细节属于载荷成型,不展开。