TencentDB Agent Memory 的 MemoryProxy 配置:哪些字段真的会读

2026-08-16

翻一个陌生项目的配置文件时,最容易犯的错是把 config.example.yaml 当成权威清单:以为里面列的就是全部可配项,以为里面的默认值就是不写时程序会用的值。TencentDB Agent Memory 的 MemoryProxy 模块恰好是个反例——截至 2026-08-16,我们在 feat/server_team 分支的快照上实读,这份示例配置有 648 行、23 个顶层 section,而代码里读的字段集合与它两个方向都对不上:示例里有代码不读的,代码里读的示例又没有。

先交代边界:这个仓库的默认分支就是 feat/server_team(不是 main、也不是 master),下文所有路径都以该分支为准。MemoryProxy 目录的 package.json 里包名是 context-proxy、版本是 0.1.0——目录名和包名不是一回事,别拿目录名去写安装命令。整个项目主模块尚处 beta 阶段,参数与接口随版本变动,下面所有字段名和取值都请以仓库最新内容为准。我们没有部署、也没有启动过这个模块,本文写的全部是文件里的静态事实。

一、先搞清楚三层优先级

src/config.tsbuildConfig 注释里把优先级写死成一句话:CLI overrides > YAML config file > defaultssrc/config.ts:260-261)。配置文件路径的默认值是 "config.yaml"src/config.ts:263)。

这里第一个容易忽略的点:命令行能覆盖的参数只有 9 个(src/config.ts:539-575)——--config--host--port--upstream--log-file--opik-url--opik-api-key--opik-enabled--verbose/-v。除此之外的任何配置项都只能走 YAML 或代码内建默认值,命令行上没有对应开关。

第二个点更关键:快照里没有 config.yaml,它被 MemoryProxy/.gitignore 排除了。也就是说,我们能看到的只有 config.example.yaml(一份示例)和 src/config.ts 里的 DEFAULT_CONFIG(一份代码内建值),真实部署上生效的那份配置,我们一个值都不知道。npm scripts 里 startnode --import tsx/esm src/index.tsstart:config 才带 --config config.yamlMemoryProxy/package.json:6-14)——不带 --config 时走的是默认路径 config.yaml,文件不在就落到 DEFAULT_CONFIG

二、同一个字段,两层的值不一样

这是本模块最需要提前知道的一件事。DEFAULT_CONFIGconfig.example.yaml 取值不同的字段,我们对照下来至少有这些(左为代码默认,右为示例配置):

字段src/config.tsconfig.example.yaml
redis.enabledfalse(34)true(116)
injection.enabledfalse(77)true(435)
sessionInit.enabledfalse(89)true(498)
tdai.enabledfalse(104)true(546)
auth.enabledfalse(136)true(322)
tdai.endpoint""(105)http://127.0.0.1:8420(547)
auth.url""(137)http://kernel.example.com:8420(323)
tdai.memory.timeoutMs3000(116)5000(558)
clickhouse.ttlDays0(31)30(313)
log.file""(13)logs(101)

还有 upstream.urltdai.apiKeycoreSkill.serviceTokenknowledge.serviceTokenstorage.cos.shark.baseUrlopik.url、以及 tdai.memoryenabled/inject/writeL0/recallL1/injectL2L3 五个布尔位(代码里全 false、示例里全 true)也在这张差异表里。

这张表怎么读?它说明「不给配置文件直接跑」和「照抄示例配置跑」得到的是两套形态完全不同的东西:前者鉴权关闭、会话初始化关闭、注入关闭、记忆链路关闭;后者全部打开。这只是两层取值的陈述,我们没有部署过,不对任何一种形态的实际行为下判断。

这里也顺带回答一个高频问题:某个开关到底该设成什么?项目没有给通用值,具体该开哪些、超时设多少,取决于你的部署形态与上游,我们不做推荐。

三、代码在读、示例里根本没有的段

比「值不一样」更隐蔽的是「示例里压根没这一段,代码却在读」。至少有三处:

costGuard 整段。 src/config.ts:65-70 定义了它的默认值(enabled:falsemarkerOptIn:falseagentProfile:"auto"options:{}),:201-232 有专门的 parseCostGuard 解析函数,src/server.ts:35:93:188:265 四处直接读 config.costGuard.*。而 config.example.yaml 里没有 costGuard: 段,全文只有 3 处提及且全在注释行(:58:60:450)。

这一段还有连锁效应:src/server.ts:35-50 有一道门控中间件,costGuard.markerOptIn 为 false 时,任何路径含 /cost-guard/ 段的请求直接返 404,错误体是 {error:"cost_guard_marker_disabled", ...}。也就是说这个在示例配置里找不到的字段,直接决定一类 URL 能不能走通。类似地,injection.assetReflection.markerOptIn 为 false 时含 /analyse/ 段的请求同样返 404,错误码 analyse_marker_disabledsrc/server.ts:60-76)——这个字段示例里倒是有(config.example.yaml:463,默认 false)。

workbuddyRequestRouting src/config.ts:144 定义 { enabled: true },解析在 :506-510。它是我们在这个模块里看到的唯一一个默认为 true 的 routing 开关,而对称的 ccRequestRouting 在示例配置里是 enabled: falseconfig.example.yaml:643-644)。示例配置中没有 workbuddyRequestRouting 这一段。

badcaseCollector parseCostGuard 会把顶层的 yaml.badcaseCollector 折叠进 costGuard.options,注释原文写它是 without being interpretedsrc/config.ts:196-199:214-216)。同一段注释还写明 parseCostGuard 只识别四个通用字段(enabled / markerOptIn / agentProfile / anthropicUpstream),其余键「every other key is kept opaque in options and handed to the extension untouched」——原样交给扩展。

需要补一句:@context-proxy/cost-guard 这个模块本身不在快照里tsconfig.json:11vitest.config.ts:11 都有指向 ./packages/cost-guard/src/index.ts 的 alias,src/storage/factory.ts:102 动态 import 它,而 packages/ 目录并不存在。代码里对此有处理说明:src/storage/factory.ts:93-96 写「cost-guard 不可用(开源用户无 submodule…)时静默跳过」。所以这些 options 里的键最终被怎么用,我们看不到,也不作推断。

其余只在代码里出现的小字段还有 redis.keyPrefix: "cg:sess:"src/config.ts:40)、langfuse.debug:20)、opik.stripRequestLogContent:19)、sessionInit.defaultTaskId:401-403)、sessionInit.debugVerboseLogging:427)。

四、示例里写着、但当前不生效的字段

反方向同样存在。示例配置自己就用 [必]/[可]/[死] 三态标了一张存储字段速查表(config.example.yaml:155-173),明确标为 [死] 的包括:storage.ttlDays 在 cos/fs/memory 后端下、storage.cos.rootPrefix所有后端下、cos.endpointDomain 在非 cos 下、sqlite.dbPath 在非 sqlite 下、fs.fsRoot 在非 fs 下。其中 cos.rootPrefix 的理由写得很直白:「shark 侧硬编码 “proxy_cache”,proxy 侧任何值都无效」(:172:222-223)。

另一处值得单独拎出来的是 tdai.memory.recallL1config.example.yaml:554 仍写着 recallL1: true # 是否启用 L1 召回(从对话中检索相关记忆);而 src/injection/index.ts:310-312 的注释原文写「L0/L1 不再每轮自动召回注入到 user prompt(会破坏 KV/prompt cache)……L1 recall injector 已下线,recallL1 配置保留但不再注册」。对应的 TdaiL1RecallInjector 仍从 src/injection/index.ts:76 导出,但 buildPipelineBundle 里没有 import、没有 register。这两处说法不一致,我们只陈述差异、标明位置,不推断原因。

还有一个跨层的例子:config.example.yaml:27forwardTimeoutMs: 600000 并注释「0 = 不超时」。src/handler.ts:353-355src/systemUserPassthrough.ts:530-532 确实用 if (forwardTimeoutMs > 0) 包住了;而 src/anthropicHandler.ts:452:493 两处是无条件调用 AbortSignal.timeout(forwardTimeoutMs) 的;src/codexHandler.tssrc/workbuddyHandler.ts 里 grep 不到这个字段,它们用的是硬编码 5 分钟的 setTimeoutsrc/codexHandler.ts:1150-1156src/workbuddyHandler.ts:673-679)。同一条注释语义在不同 handler 路径上覆盖面不同,这也是只陈述、不延伸。

五、开关为真 ≠ 组件被装上

injection.injectors 在示例里是 ["skill","knowledge","tdai-memory"]config.example.yaml:436),但列表里写了名字只是必要条件之一。src/injection/index.ts 里的注册前置条件是叠加的:

  • knowledge 需要同时满足 injectors"knowledge"knowledge.enabledknowledge.serviceToken 非空三项(src/injection/index.ts:453-457shouldRegisterKnowledgeInjector)。而 knowledge.enabled 在示例里是 falseconfig.example.yaml:590),knowledge.serviceToken 的代码默认是空字符串(src/config.ts:128)。
  • tdai-memory 要求 injectors"tdai-memory" tdai.enabled tdai.memory.enabled tdai.memory.inject 四者全真(src/injection/index.ts:288);其中 TdaiProfileMemoryInjector 还额外要求 tdai.memory.injectL2L3:307-309)。
  • skill 相对简单:injectors"skill" 就注册 SkillInjectorSkillToolsInjector 两个(:262-275)。

所以排查「某个注入块没出现」时,只看 injectors 数组是不够的,要沿着这几个布尔位一路查下去。

六、两个写法上的坑

段名兼容。 config.example.yaml:571 的注释写:skill: 段也可写作 coreSkill:(旧名,仍被读取);两者同时存在时 skill: 优先。也就是说,你可能在不同来源的配置片段里见到两个名字,它们指的是同一段。

环境变量展开的作用域极窄。 ${VAR} / ${VAR:-default} 这种写法只对 systemUsers 段生效src/config.ts:514-528 的注释原文是「Deliberately scoped narrowly (only called from systemUsers today)」。别指望在其它段里写 ${VAR} 能被展开。示例配置里 systemUsers 那条 name: memory 的条目也确实是用 ${VAR} 占位写 userId/displayName/userKey 的(config.example.yaml:362-365);而 src/config.ts:479-486 会把 userId 为空的条目静默丢弃。

另外,src/ 里以字面量形式出现的环境变量有 PROXY_DB_PATHPROXY_DEBUG_DUMP_OUTBOUND_MD5PROXY_DEBUG_DUMP_INBOUNDPROXY_DEBUG_DUMP_BODYTDAI_PROXY_ADMIN_API_KEYPROXY_DEBUG_ASSET_REFLECTIONCC_FORM_MODE。其中 PROXY_DEBUG_DUMP_BODYPROXY_DEBUG_DUMP_INBOUNDPROXY_DEBUG_DUMP_OUTBOUND_MD5PROXY_DEBUG_ASSET_REFLECTIONCC_FORM_MODEREADME.mdconfig.example.yaml 里 grep 均为 0 次,README 的环境变量小节只列了 4 个(MemoryProxy/README.md:233-238)。CC_FORM_MODE 自己的注释还明标 EXPERIMENTALsrc/session/form.ts:388-402)。

管理端密钥的优先级示例配置写得很清楚:env TDAI_PROXY_ADMIN_API_KEY > yaml admin.apiKey > 默认 ""config.example.yaml:347:349)。真实密钥请走你自己的托管方式,配置示例里请保留 <YOUR_API_KEY> 这类占位。

七、自己怎么核这份配置

不必相信本文的结论,给三个可执行动作:

  1. 打开 MemoryProxy/src/config.ts,把 DEFAULT_CONFIG:9-145)整段与 config.example.yaml 并排看一遍,凡两边都有、值不同的,记下来。
  2. MemoryProxy/src/ 下检索 config. 开头的读取点,凡读到的键在 config.example.yaml 里搜不到的,就是「代码读、示例不给」的那一类(costGuard 是最大的一块)。
  3. 反过来,把 config.example.yaml 里的字段名逐个拿去 src/ 检索,搜不到的要么是被 [死] 标注过的,要么就落进本文第四节那类情况。

最后提醒一句:这个模块会经手团队与 Agent 的对话内容,涉及敏感数据;配置里的上游地址、鉴权开关、日志与追踪后端如何设置,请结合自身合规要求评估,本文只是把仓库里的字段读了一遍。

延伸阅读


本文依据 TencentDB Agent Memory 官方仓库(github.com/TencentCloud/TencentDB-Agent-Memoryfeat/server_team 分支上的 README、INSTALL、CHANGELOG、ROADMAP 与四个模块的源码整理, 核对日 2026-08-16,对应仓库快照 97f9465。该仓库的默认分支即为 feat/server_team。 本文内容为仓库源码与文档口径,我们没有部署、也没有运行过该项目的任何一个模块, 因此不涉及运行效果、检索质量与性能的任何描述。 该项目主模块处于 beta 阶段、其余模块版本号仍为 0.1.0,参数与接口随版本变动,请以仓库最新内容为准。 该项目会采集并存储团队的对话、文档与代码,属于敏感数据,是否使用请结合自身合规要求评估。 安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。