1. 从一次 Agent 失忆说起:为什么记忆机制绕不开向量数据库
如果你正在做 AI Agent Harness Engineering,大概率遇到过这种场景:Agent 在同一个会话里表现聪明,一旦跨会话、跨任务,就像换了个人。上一轮已经确认过的接口约定、用户偏好、踩过的坑,下一轮全部归零。这不是模型不行,而是 Harness 层没有把「记忆」当成一等公民来设计。
AI Agent 的记忆机制,本质是解决三件事:写入什么、存到哪里、怎么召回。写入决定记忆质量,存储决定可扩展性,召回决定 Agent 是否真的「记得住」。在这条链路里,向量数据库(Vector DB)承担的是语义记忆的载体角色——把对话片段、工具调用结果、任务摘要转成 embedding 存起来,需要时用相似度检索把相关记忆捞回来,拼进上下文。
这篇聚焦落地:给你一份可复制的config.toml骨架,用 TaoToken 统一 Key/API 通道接入 embedding 与对话模型,再给一套记忆召回验证动作,让你在本地把 Agent 记忆检索流程跑通。适合已经写过基础 Agent loop、准备补上长期记忆层的开发者。读完你能得到一个能写入、能召回、能验证的最小记忆系统。
2. TaoToken 前置:统一 Key 与 API 通道
在拆记忆链路之前,先把模型通道理顺。Agent 记忆系统至少要调两类模型:embedding 模型(把文本转向量)和对话模型(生成回答)。如果每个模型都单独配 Key、单独改 base_url,Harness 配置会迅速失控。TaoToken 的价值就在这里:一个 Key、一个 API 入口,同时覆盖 embedding 和对话调用,配置层只需要维护一份凭证。
你需要先拿到 Key。登录官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,进入控制台创建 API Key。控制台地址:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。Key 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,建议给记忆服务单独建一个 Key,方便按项目隔离和轮换。
API 基地址统一用 https://taotoken.net/api ,注意这个地址不带 UTM 参数,直接写进配置即可。接入细节和参数说明可以对照文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。如果你后面要做长期编码类 Agent,可以了解 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。
注意:Key 只放在环境变量或本地配置文件里,不要提交到 Git。记忆系统里往往存着用户对话,凭证泄露的后果比普通项目更严重。
3. 可复制配置:config.toml 骨架与记忆链路
下面这份config.toml是记忆系统的配置骨架,分三块:模型通道、向量库、记忆策略。你可以直接复制后改字段值。
# config.toml —— Agent 记忆系统配置骨架 [llm] # TaoToken 统一通道 base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" # 从环境变量读取,不写死 chat_model = "gpt-4o-mini" # 对话模型 embedding_model = "text-embedding-3-small" # 嵌入模型 timeout_seconds = 60 max_retries = 3 [vector_db] provider = "qdrant" # 本地可用 qdrant / chroma / milvus host = "127.0.0.1" port = 6333 collection = "agent_memory" vector_size = 1536 # 必须与 embedding 模型输出维度一致 distance = "Cosine" # 语义检索常用余弦距离 [memory] # 写入策略 write_batch_size = 8 # 攒够 8 条再批量写入 summary_trigger_tokens = 2000 # 会话超长时先摘要再入库 # 召回策略 top_k = 5 # 每次召回条数 score_threshold = 0.35 # 低于该相似度丢弃,避免噪声 recency_weight = 0.2 # 时间衰减权重,越新越优先 # 生命周期 ttl_days = 90 # 记忆过期天数 dedup_threshold = 0.95 # 相似度高于此值视为重复,跳过写入几个字段值得展开。vector_size必须和 embedding 模型输出维度严格一致,text-embedding-3-small是 1536 维,写错会在建集合时报维度不匹配。score_threshold是召回质量的关键闸门,设太低会把无关记忆塞进上下文,反而干扰模型;设太高会漏召回。dedup_threshold用来防止同一句话反复入库,Agent 循环里很容易出现重复写入。
记忆读写链路可以这样理解:用户消息进来 → 判断是否值得记(是否包含事实、偏好、结论)→ 调 embedding 模型转向量 → 写入向量库并附带元数据(user_id、timestamp、type)→ 下一轮对话前,把当前 query 转向量 → 按 top_k 召回 → 按 score_threshold 过滤 → 拼进 prompt。向量库在这里不是「数据库」,而是语义索引层,元数据过滤和向量检索要配合使用。
4. 验证请求:把记忆召回跑通
配置写完必须验证,否则你不知道是写入失败还是召回失败。分三步:先验证模型通道,再验证写入,最后验证召回。
第一步,验证 TaoToken 通道能同时调通 embedding 和对话。用 curl 测 embedding:
export TAOTOKEN_API_KEY="你的Key" curl -s https://taotoken.net/api/v1/embeddings \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "text-embedding-3-small", "input": "用户偏好使用 PostgreSQL 而不是 MySQL" }' | head -c 300返回里应包含data[0].embedding数组,长度 1536。如果返回 401,检查 Key;返回 404,检查 base_url 是否漏了/v1路径。
第二步,写入一条记忆并确认落库。用 Python 脚本走完整链路:
import os, requests, uuid, time BASE = "https://taotoken.net/api" KEY = os.environ["TAOTOKEN_API_KEY"] HEADERS = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"} def embed(text: str): r = requests.post(f"{BASE}/v1/embeddings", headers=HEADERS, json={"model": "text-embedding-3-small", "input": text}) r.raise_for_status() return r.json()["data"][0]["embedding"] def write_memory(qdrant_url, text, user_id): vec = embed(text) point = { "id": str(uuid.uuid4()), "vector": vec, "payload": {"text": text, "user_id": user_id, "ts": int(time.time())} } r = requests.put(f"{qdrant_url}/collections/agent_memory/points", params={"wait": "true"}, json={"points": [point]}) r.raise_for_status() return point["id"] def recall(qdrant_url, query, user_id, top_k=5, threshold=0.35): vec = embed(query) body = { "vector": vec, "limit": top_k, "with_payload": True, "filter": {"must": [{"key": "user_id", "match": {"value": user_id}}]}, "score_threshold": threshold } r = requests.post(f"{qdrant_url}/collections/agent_memory/points/search", json=body) r.raise_for_status() return r.json()["result"] if __name__ == "__main__": Q = "http://127.0.0.1:6333" write_memory(Q, "用户偏好使用 PostgreSQL 而不是 MySQL", "u_001") write_memory(Q, "项目部署在 Kubernetes 上,命名空间是 prod", "u_001") hits = recall(Q, "数据库选型有什么偏好", "u_001") for h in hits: print(round(h["score"], 3), h["payload"]["text"])第三步,看召回结果。查询「数据库选型有什么偏好」,理想输出是 PostgreSQL 那条排第一,score 明显高于 Kubernetes 那条。如果两条分数接近,说明 embedding 区分度不够或阈值设置有问题;如果一条都没召回,先确认写入是否成功(查 collection 的 points 数量),再确认 filter 里的 user_id 是否一致。
实测下来,score_threshold在 0.3 到 0.4 之间对中文对话记忆比较稳。你可以先用一批已知相关的句子对,跑出分数分布,再定阈值,而不是拍脑袋。
5. 本篇常见错排查
维度不匹配:建 collection 时vector_size和 embedding 输出维度不一致,写入直接报错。解决方式是先调一次 embedding 接口,打印len(embedding),用这个值建集合。
召回为空但写入成功:最常见原因是 filter 条件写错,比如写入时user_id是字符串,查询时传了数字;或者score_threshold设太高。先把 threshold 设为 0 试一次,确认能召回后再逐步调高。
重复记忆堆积:Agent 每轮都把相同内容写入,导致召回结果全是重复项。用dedup_threshold在写入前做一次相似度检查,命中就跳过。
上下文被记忆淹没:top_k 设太大,召回十几条无关记忆,反而稀释了当前任务信息。记忆召回要克制,5 条以内通常够用,配合 score_threshold 做质量过滤。
时间衰减没生效:recency_weight只是配置项,需要在召回后自己按ts重排序。向量库返回的是相似度排序,时间维度要应用层补上。
Key 混用:embedding 和对话用了不同 Key,轮换时漏改一个。统一走 TaoToken 一个 Key,配置里只维护api_key_env一处。
6. 下一步:把记忆接进你的 Agent loop
配置和验证跑通后,把记忆层接进 Agent 主循环就三步:每轮对话结束前判断是否写入记忆;每轮对话开始前召回相关记忆拼进 system prompt;定期清理过期记忆。写入判断可以用一个轻量规则——包含事实、偏好、结论、工具返回的关键结果就写,纯寒暄不写。
如果你要验证不同模型在记忆召回场景下的表现,可以直接用模型对话页做对比:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 。长期跑编码类 Agent、需要稳定记忆通道的,看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。接入参数和报错对照文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。Key 在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 管理。
最后留一个实用技巧:记忆系统的调试,别只看最终回答,要把召回结果打印出来。Agent 答得不对,八成是召回错了,而不是模型不行。把召回日志当成一等公民,你的 Harness 会稳很多。