1. 先管住模型出口:WeKnora 的 Fact 记忆为什么容易烧 Token
最近微信团队开源的 WeKnora 把企业知识库往 Agent 知识层推进,长期记忆里的 Fact 类信息很值得企业知识 Agent 开发者先动手验证。你要复现“从对话里抽取 Fact,等待用户确认,再按需召回”这条链路,第一件事不是改 Prompt,而是把模型调用出口固定到 TaoToken:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=weknora_fact_intro 。TaoToken 的模型调用地址是 https://taotoken.net/api ,Key 去控制台创建。这样做不是因为“额度不够”这种泛泛理由,而是 Fact 抽取天然是一个高频、可批处理、又需要审计的调用场景:每轮对话结束可能触发候选事实抽取,候选事实还要去重、合并、等待确认,确认之后在后续问题里又会被召回。只要 Agent 还在跑,Token 消耗就不是一次性的,而是持续发生的。
WeKnora 本身把企业知识库常见能力做得很全,包括多格式文档解析、混合检索、Rerank、知识图谱、数据源接入,以及 ReAct Agent、Skills、沙箱等扩展。但对企业知识 Agent 开发者来说,Fact 类记忆是最容易“写错”的一类数据。Preference 写错了,最多影响推荐风格;Task 写错了,最多影响一次执行;Fact 写错了,可能会被后续几十次对话反复召回,最后变成企业知识层里的污染源。所以原文里“Agent 自动提取出的记忆不会直接写入,而是等待用户确认”这个设计非常关键。它把模型的不确定性挡在长期记忆之外,也给开发者留下了接入自己的鉴权、审计、成本控制的空间。
这也是为什么建议你在本地开发阶段就把 TaoToken 放到模型调用链路上。TaoToken 管住的不只是 Key,还包括调用入口、模型选择和用量观测。Fact 抽取可以用便宜模型做初筛,确认环节可以用更强模型做复核,召回阶段则尽量走本地缓存或向量检索,不要每次都把全量历史记忆塞进上下文。下面这套写法,目标就是让你在 WeKnora 的 Fact 记忆链路上,既能复现确认流程,又能把 Token 消耗控制在可解释范围内。
2. 拆开 WeKnora 的 Fact 抽取链路:触发、候选、确认、召回
WeKnora 的长期记忆里会区分 Profile、Preference、Fact、Task、Interest 等类型。Fact 不是“用户喜欢什么”,也不是“用户让你做什么”,而是“用户明确表达过的、可被证伪的客观信息”。例如“我们团队使用 GitLab 做代码托管”“这个项目的发布窗口是每周四”“该客户要求发票走电子专票”。这些信息未来会被多次复用,所以它们必须经过确认。
把链路拆开,大致是下面六段:
| 阶段 | WeKnora 侧动作 | 企业知识 Agent 开发者要补的能力 | TaoToken 侧可观测点 |
|---|---|---|---|
| 触发 | 对话轮次结束、用户手动保存、特定意图命中 | 决定哪些 Session 触发抽取,避免每句话都调用模型 | 调用次数、模型分布、单次 Token |
| 候选抽取 | 从对话中提取 Fact 候选 | 用 JSON Schema 约束输出,过滤偏好和任务 | 抽取模型用量、失败率 |
| 去重合并 | 与已有记忆比对,判断新增或更新 | 本地做向量相似度或规则匹配,减少模型调用 | 去重阶段是否重复调用 |
| 等待确认 | 候选记忆进入待确认队列 | 在 UI/IM/工单里呈现“原文摘录 + 抽取结果” | 确认前后调用量对比 |
| 写入召回 | 用户确认后写入长期记忆,后续按需召回 | 设置作用域、过期时间、召回上限 | 召回是否携带过多历史 |
| 审计回溯 | 记录来源、确认人、版本 | 本地日志与 TaoToken 调用记录对齐 | 可按 Key、模型、时间排查 |
这里最容易出问题的是“触发”和“去重合并”。如果每轮对话都调用模型抽 Fact,用量会很快上去;如果去重也完全交给大模型,等于一次抽取变成两次模型调用。更合理的做法是:抽取只处理最近一轮对话,并且只输出 Fact 候选;去重先用本地向量或简单规则初筛,只有相似度处于模糊区间时,才让模型判断“新增、更新还是忽略”。
WeKnora 的确认流程给了你一个很好的插入点。候选记忆不要直接写入长期库,而是落到一张待确认表。待确认表可以放在本地 SQLite 里,由你的服务读取,再展示给用户确认。注意,不要让 Agent 或 MCP 直接去连生产数据库执行写操作。企业知识层里的写入动作应该由你的后端服务完成,SQL 也由读者在本地或测试环境手工执行。生产库的权限边界不能交给模型。
一个更具体的流程对照如下:
对话结束 -> 判断是否触发 Fact 抽取 -> 调用 TaoToken 上的轻量模型,输出 JSON 候选 -> 本地规则过滤:去掉 Preference、Task、Interest、Profile -> 本地向量去重:与已确认 Fact 比对 -> 写入待确认队列 -> 给用户展示:原文摘录 / 抽取 Fact / 作用域 / 过期时间 -> 用户点击确认或拒绝 -> 确认后写入长期记忆;拒绝后记录负样本 -> 后续对话召回时,只取 top-k 已确认 Fact这套流程和 WeKnora 原生“先等待用户确认,再自动召回”的思路一致,但额外加了两层:本地去重和成本观测。TaoToken 在这里的角色不是替代 WeKnora,而是把模型调用集中到一个可管理的出口。你可以去 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=weknora_fact_pipeline 创建 Key,然后在环境变量里统一注入,避免每个 Agent、每个脚本各写一套 Key。
3. 可复现的 Fact 抽取 Prompt:只抽事实,不碰偏好和任务
要复现 Fact 抽取,Prompt 必须比“请提取记忆”这种自然语言指令严格得多。推荐直接要求模型输出 JSON 数组,并且把字段固定下来。下面这段 Prompt 片段可以放在 WeKnora 的自定义记忆抽取节点里,也可以放在你自己的 Agent 服务里。
你是企业知识 Agent 的长期记忆抽取器。你的任务是从最近一轮对话中,只抽取 Fact 类长期记忆。 Fact 的定义: - 可在未来复用; - 可被证伪; - 不是用户喜好、不是一次性任务、不是兴趣标签、不是个人档案; - 必须能在原文中找到明确依据。 禁止抽取: - Preference:喜欢/讨厌/偏好某风格; - Task:让 Agent 执行的一次性动作; - Interest:关注某个话题; - Profile:姓名、职位等身份档案,除非用户明确要求作为事实保存。 输出格式:JSON 数组。没有候选时输出 []。 每个对象包含: { "type": "fact", "subject": "主体", "predicate": "关系或属性", "object": "客体或值", "fact_text": "合并后的事实句", "confidence": 0.0, "source_quote": "原文中的短句,不超过 80 字", "scope": "user | team | org", "expires_at": null, "needs_confirmation": true } 规则: 1. confidence 低于 0.65 不要输出。 2. source_quote 必须来自最近一轮对话,不得改写。 3. scope 无法判断时填 user。 4. expires_at 只接受 ISO 日期或 null。 5. 所有输出都必须 needs_confirmation=true。 6. 不要输出解释,不要输出 Markdown,只输出 JSON。这段 Prompt 的重点不是“更聪明”,而是“更窄”。Fact 抽取最怕模型自由发挥,把“用户说喜欢简洁回答”写成 Fact,或者把“帮我查一下明天天气”写成 Task 后误存成长期事实。通过 JSON Schema 和禁止类型,可以大幅降低污染。
接下来用 TaoToken 的兼容接口调用。Base URL 使用 https://taotoken.net/api ,Key 用 YOUR_API_KEY 占位。下面是一个 Python 侧的例子,演示如何做抽取和后处理。具体 SDK 可以按你项目里已有依赖替换,核心是 base_url 和 api_key 的来源。
import json import os from openai import OpenAI client = OpenAI( base_url="https://taotoken.net/api", api_key=os.environ.get("TAOTOKEN_API_KEY", "YOUR_API_KEY"), ) FACT_PROMPT = """你是企业知识 Agent 的长期记忆抽取器。 只抽取 Fact 类长期记忆,输出 JSON 数组。 字段:type, subject, predicate, object, fact_text, confidence, source_quote, scope, expires_at, needs_confirmation。 没有候选输出 []。不要输出解释。""" def extract_fact_candidates(last_dialogue: str): resp = client.chat.completions.create( model="你的抽取模型ID", messages=[ {"role": "system", "content": FACT_PROMPT}, {"role": "user", "content": f"最近一轮对话:\n{last_dialogue}"}, ], temperature=0.1, ) content = resp.choices[0].message.content.strip() candidates = json.loads(content) facts = [] for item in candidates: if item.get("type") != "fact": continue if float(item.get("confidence", 0)) < 0.65: continue if not item.get("source_quote"): continue item["needs_confirmation"] = True facts.append(item) return facts如果你在 WeKnora 里做二次开发,建议把extract_fact_candidates的返回值写入待确认表,而不是直接写长期记忆。确认动作由用户完成,写入动作由后端服务完成。模型只负责候选,不负责最终事实。这个边界一旦守住,后面排障会轻松很多。
TaoToken 在这里的价值是让你能按 Key 观察抽取调用。你可以给“Fact 抽取”单独创建一个 Key,给“对话主模型”创建另一个 Key,给“确认后摘要”再创建一个 Key。这样当用量异常时,你能快速判断是抽取太频繁,还是主对话太长。需要创建多个 Key 时,从 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=weknora_fact_keys 进入控制台即可。
4. Claude Code、Codex、CC Switch 接入 TaoToken:配置分三件套,不要混用
企业知识 Agent 开发者往往不只用一个工具。你可能用 Claude Code 改后端,用 Codex 写配置,用 CC Switch 在多个供应商之间切换。这里必须强调:Claude Code 用 Anthropic 风格的环境变量,Codex 用 OpenAI 风格的 config.toml,不要把ANTHROPIC_*套到 Codex 上。混用最常见的后果是请求地址不对、模型名不识别、401 和 404 交替出现。
4.1 Claude Code:settings.json 与 ANTHROPIC_*
Claude Code 推荐在项目或用户级settings.json里配置。Base URL 用 https://taotoken.net/api ,Key 用 YOUR_API_KEY 占位。
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "你的Claude模型ID" } }如果你不想把 Key 写进文件,可以只保留ANTHROPIC_BASE_URL和ANTHROPIC_MODEL,然后用系统环境变量注入ANTHROPIC_AUTH_TOKEN。例如在本地 shell 里:
export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_AUTH_TOKEN="YOUR_API_KEY" export ANTHROPIC_MODEL="你的Claude模型ID"验证时不要只看 Claude Code 是否启动,要看它发出的请求是否命中 TaoToken。你可以先在模型对话页做一次最小调用,确认 Key 可用,再去配 Claude Code。模型对话入口在文末 CTA 里。
4.2 Codex:config.toml 用 model_provider
Codex 的配置逻辑和 Claude Code 不同。它通常通过config.toml声明 provider,再用环境变量读取 Key。下面是一个示例,Base URL 仍然是 https://taotoken.net/api ,但不要写ANTHROPIC_*。
model = "你的Codex模型ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"然后在 shell 里设置:
export TAOTOKEN_API_KEY="YOUR_API_KEY"如果你使用的是支持 Responses API 的 Codex 版本,wire_api可以按你本地版本要求改成responses。但无论怎么改,Key 的环境变量名要和env_key一致,base_url不要多加/v1或重复路径。TaoToken 的统一入口是 https://taotoken.net/api ,路径拼接交给客户端。
4.3 CC Switch 三件套:Claude、Codex、环境变量档案
如果你用 CC Switch 做多工具切换,建议把它当成“三件套”来维护:
- Claude Code 档案:只放
ANTHROPIC_BASE_URL、ANTHROPIC_AUTH_TOKEN、ANTHROPIC_MODEL。 - Codex 档案:只放
model_provider、base_url、env_key、model。 - 通用环境变量档案:放
TAOTOKEN_API_KEY=YOUR_API_KEY,供 Codex 和其他脚本读取。
不要为了省事,把 Claude 的ANTHROPIC_AUTH_TOKEN直接填到 Codex 的env_key里。Codex 读的是TAOTOKEN_API_KEY这类变量,不是ANTHROPIC_*。如果你在 CC Switch 里切换后出现配置不生效,优先检查它是否覆盖了项目目录下的settings.json或config.toml。
需要统一管理 Key 时,从 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=weknora_fact_ccswitch 进入控制台创建。建议给 Claude Code、Codex、Fact 抽取服务分别建 Key,不要所有工具共用一个 Key。共用 Key 虽然省事,但一旦某个 Agent 进入循环调用,你很难定位是谁在消耗。
5. 记忆确认流程对照:WeKnora 原生确认与代理侧审计怎么配合
WeKnora 的长期记忆设计里,自动抽取的候选不会直接落库,而是等待用户确认。这个机制解决的是“模型抽取结果是否可信”的问题。但企业开发者还要解决另一个问题:确认之后,如何知道这条 Fact 是被哪个模型、哪次对话、哪个 Key 抽取出来的。把 WeKnora 原生确认和 TaoToken 代理侧审计对齐,才能形成闭环。
下面是一个流程对照表,你可以直接拿去做实现清单:
| 环节 | WeKnora 原生行为 | 你需要在本地补的记录 | TaoToken 侧对应信息 |
|---|---|---|---|
| 抽取候选 | 从对话中提取记忆候选 | 记录 session_id、message_id、抽取时间 | 调用时间、模型、Token 数 |
| 类型过滤 | 区分 Profile、Preference、Fact 等 | 只让 Fact 进入待确认队列 | 抽取 Key 的调用量 |
| 用户确认 | 等待用户确认后写入 | 记录确认人、确认时间、原文摘录 | 确认动作本身不一定调模型 |
| 写入长期记忆 | 按需召回 | 记录 scope、expires_at、版本号 | 写入后召回调用单独计量 |
| 后续召回 | 自动召回已确认记忆 | 设置 top-k 和最大 Token | 召回调用是否过大 |
| 拒绝样本 | 不写入 | 记录拒绝原因,反哺 Prompt | 抽取失败率、重复率 |
一个很实用的做法是给待确认表加字段:
-- 本地 SQLite 示例,仅用于开发环境手工执行 CREATE TABLE fact_candidates ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, message_id TEXT NOT NULL, fact_text TEXT NOT NULL, subject TEXT, predicate TEXT, object TEXT, confidence REAL, source_quote TEXT, scope TEXT DEFAULT 'user', expires_at TEXT, status TEXT DEFAULT 'pending', created_at TEXT DEFAULT CURRENT_TIMESTAMP, confirmed_at TEXT, confirmed_by TEXT, reject_reason TEXT );注意,这只是本地开发示例。不要让 Agent 或 MCP 直接拿这段 SQL 去连生产库执行。生产环境的表结构、权限和迁移流程应该由你的后端工程管理。模型只输出候选 JSON,写入由服务层控制。
确认流程可以设计成“两段式”:
第一段,抽取后立即在待确认表写入pending记录,并把source_quote展示给用户。用户看到的不是模型改写后的事实句,而是原文摘录加结构化字段。这样用户能判断模型有没有断章取义。
第二段,用户点击确认后,服务层把status改为confirmed,再写入长期记忆库。如果用户点击拒绝,把status改为rejected,并记录reject_reason。这些拒绝样本可以定期用来优化 Prompt,比如发现模型总把“今天先这样”抽成 Fact,就在 Prompt 里增加反例。
召回时也要控制上下文。不要因为长期记忆里有 500 条 Fact,就每次都全部塞给模型。先按用户、团队、组织作用域过滤,再做向量检索,最后只取 top-k。TaoToken 的用量页可以帮助你观察召回调用是否突然变大。如果你发现对话主模型 Token 暴涨,但抽取 Key 用量正常,问题通常出在召回上下文组装,而不是 Fact 抽取本身。
6. 排障顺序:Fact 抽取重复、确认不生效、Token 异常
Fact 记忆链路出问题时,不要一上来就改 Prompt。先按“调用是否通、输出是否合法、写入是否发生、召回是否过量”四层排查。下面这张表可以直接用。
| 现象 | 优先检查 | 常见原因 | 处理方式 |
|---|---|---|---|
| 401 Unauthorized | Key 是否有效、环境变量是否注入 | 用了旧 Key,或 CC Switch 覆盖了配置 | 重新创建 Key,确认TAOTOKEN_API_KEY |
| 404 Not Found | Base URL 是否写成 https://taotoken.net/api | 多加了/v1或路径重复 | 保持 Base URL 不加 UTM、不重复拼接 |
| 429 Too Many Requests | 抽取触发是否过频 | 每轮对话都调用,或循环调用 | 加节流、批量、本地去重 |
| Fact 重复写入 | 去重逻辑是否执行 | 相似度阈值太低,模型判断不稳定 | 先用本地向量初筛,再让模型复核 |
| 确认后不生效 | 写入服务是否执行 | 只改了待确认表状态,没写长期库 | 服务层事务处理,记录写入日志 |
| 召回内容过长 | top-k 是否过大 | 把全部 Fact 塞进上下文 | 限制条数、按 scope 过滤、摘要化 |
| Claude Code 配置不生效 | settings.json 是否被覆盖 | CC Switch 或环境变量优先级问题 | 检查项目级、用户级、环境变量顺序 |
| Codex 报模型不存在 | config.toml 的 provider 是否正确 | 把ANTHROPIC_*写进了 Codex | 改用model_provider+env_key |
| Token 异常升高 | 抽取 Key 与主对话 Key 是否混用 | 多工具共用 Key,无法归因 | 拆分 Key,按服务观察用量 |
关于 Base URL,再强调一次:TaoToken 的模型调用地址是 https://taotoken.net/api ,这个地址在配置里不要加 UTM 参数,也不要写成别的路径。UTM 只用于官网入口和文档页统计。你在 Claude Code、Codex、脚本里填的 Base URL 都应该是干净地址。
如果 Fact 抽取重复率很高,可以做一个本地相似度阈值实验。比如先用向量模型计算候选 Fact 与已确认 Fact 的余弦相似度:
# 伪代码:本地去重,不调用远程模型 def should_ask_llm(candidate, existing_facts, embed, cosine): for fact in existing_facts: score = cosine(embed(candidate["fact_text"]), embed(fact["fact_text"])) if score > 0.92: return "duplicate", fact if score > 0.78: return "ask_llm", fact return "new", None相似度大于 0.92 的直接视为重复,不再调用模型;0.78 到 0.92 之间才让模型判断“更新还是忽略”;低于 0.78 的直接作为新候选。这样能把大量重复判断挡在模型之外,TaoToken 上的抽取调用量会明显更稳定。
如果确认流程不生效,检查服务层是否真的写了长期记忆库。很多实现只把待确认记录状态改成confirmed,但召回逻辑读的是另一张表。可以加日志:候选 ID、确认人、写入表、写入时间、召回命中次数。这样一旦用户说“我明明确认过”,你可以快速定位是没写入,还是写入了但召回没命中。
7. 上线前检查清单与高转化接入路径
在上线 WeKnora 风格的 Fact 长期记忆之前,建议按下面清单过一遍:
- Fact 抽取只处理最近一轮或最近一个窗口,不要全量历史反复跑。
- Prompt 必须限定
type=fact,并明确禁止 Preference、Task、Interest、Profile。 - 候选结果必须进入待确认队列,
needs_confirmation=true不允许绕过。 - 去重先用本地向量或规则,模糊区间再调用模型。
- 确认写入由后端服务完成,模型和 MCP 不直接写生产库。
- 召回设置 top-k、scope 过滤和最大 Token,避免上下文膨胀。
- 给 Fact 抽取、主对话、确认摘要分别创建 TaoToken Key,方便归因用量。
- Claude Code 用
settings.json和ANTHROPIC_*;Codex 用config.toml和model_provider;CC Switch 维护三件套档案,不要混用。
如果你还没有 Key,建议按下面路径走,顺序不要颠倒:
- 先到模型对话页做一次最小调用,确认模型和 Key 可用:https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=weknora_fact_chat
- 如果准备日常开发使用,查看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=weknora_fact_plan
- 进入控制台创建独立 Key,给 Fact 抽取服务和编码工具分开:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=weknora_fact_keys
- Claude Code 用户参考文档完成 settings.json 配置:https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=weknora_fact_claudecode
最后再回到 WeKnora 的 Fact 记忆本身。企业知识 Agent 的长期记忆不是“记得越多越好”,而是“记得越准越好”。Fact 类记忆如果直接自动写入,短期看起来 Agent 更聪明,长期看却会增加召回噪声和 Token 成本。把抽取、确认、去重、召回拆成可控步骤,再用 TaoToken 统一模型出口和用量观测,你就能在复现 WeKnora 这类企业知识底座能力时,既保留 Agent 的记忆体验,又不让记忆系统变成新的黑盒成本中心。官网入口在这里:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=weknora_fact_final ,需要进一步看模型、创建 Key、配置 Claude Code 时,按文末路径逐项完成即可。