自托管一套给内部 Agent 用的 OpenViking,容易让人返工的不是模型调用,而是两件很基础的事:数据落在哪里、不同项目和会话靠什么键分开。这两件事建议在第一次启动前就定下来,因为隔离键一旦随数据写进存储,后面再改就同时涉及写入路径、检索路径和已有数据。可以先按“存储位置 → 隔离键 → 端口与鉴权”的顺序填配置,再用两条不同键的数据做一次交叉读回,确认隔离是真的生效,而不是只写在文档里。
自托管 OpenViking 时,先用固定字段确定存储后端与数据目录,再为每个请求生成 tenant/project/session 三段式隔离键,且写入端与检索端必须调用同一段拼接逻辑。最小配置能启动、日志能打印监听地址和存储路径,只说明链路通了;隔离是否成立,要靠写入两条不同键的数据再交叉读回来判断。若交叉读取命中他人数据,先查存储过滤条件有没有带上该键,再怀疑检索实现。
列出部署前必须确定的配置项
装完再改存储位置,通常意味着数据要迁移或者重灌;装完再改鉴权方式,已经接进来的 Agent 要重发一遍凭证。所以下面三类配置建议在部署前落到一份文件里,即使值先用占位符。
- 存储位置:决定向量数据、元数据和会话记录的落点。占位字段可以是
storage.data_dir(本地卷,适合单机试跑)、storage.metadata_dsn(外部数据库,适合多副本)、storage.index_path(索引与元数据分离时用)。填之前先确认容器内路径和宿主机挂载是否一致,否则重启后数据可能落在容器可写层里。 - 服务端口:占位字段
server.host/server.port。端口本身好改,但它决定了后续健康检查和写入请求的地址,早定下来能少改一次调用方配置。 - 鉴权方式:占位字段
auth.mode(如 token / mTLS / 网关透传)与auth.token_source。内部 Agent 场景常见做法是由网关统一校验、服务端只信任透传的身份头,但这需要和你们的网络边界一起确认,不要默认服务端放在内网就可以不校验。
填写顺序建议:先写存储位置并确认挂载可写,再写鉴权,最后写端口。原因是存储位置错了要靠数据迁移补救,端口错了改一行配置即可。
设计会话与项目的隔离键
隔离键不是一个自由字符串,而是一段由固定字段拼出来的标识。建议拆成三段:tenant(团队或环境)、project(业务项目)、session(一次对话或一个 Agent 任务),拼接成 tenant:project:session。
命名规则可以先收敛成这样几条:
- 每段只允许小写字母、数字、下划线和短横线,各段长度不超过 64 字符,避免斜杠和冒号出现在段内(冒号是分隔符)。
- 空段不允许,缺失就拒绝写入,不要用
default自动补——补了就等于把两个项目合到一起。 - 拼接固定用冒号,顺序固定,不要一处写
project:session、另一处写session:project。
关键点在于一致性:写入时用什么键,检索时就得用同样的键。落地做法是把拼接逻辑收进一个函数(例如 build_scope_key(tenant, project, session)),写入入口和检索入口都调用它,不要在两处各手拼一次。隔离键建议由服务端根据已鉴权的身份生成,或者至少在服务端重新拼装一次,而不是直接信任客户端传来的整串键,否则任何调用方都能构造出别人项目的键。
如果存储层支持按字段过滤(多数向量库的 metadata filter 或关系表列),把这三段分别存成独立字段比只存一个拼接串更好排查问题;如果只能存一个字符串,也要保证前缀顺序固定,便于按前缀做范围过滤。
用一份最小配置把服务拉起来
这一步的目的只有一个:确认部署链路本身是通的,不涉及隔离验证。字段名以你实际拿到的配置模板为准,下面只是占位骨架。
# config.yaml(字段名按下发模板替换)
server:
host: 0.0.0.0
port: 8080
storage:
data_dir: /var/lib/openviking/data
metadata_dsn: "" # 留空表示用本地元数据
auth:
mode: token
token_source: env
log:
level: info
启动命令按你的交付形态二选一:
# 二进制方式
./openviking serve `--config` ./config.yaml
# 容器方式
docker compose up -d
docker compose logs -f openviking
启动成功时,日志里通常能看到三类信息,字段名可能不同,但语义要对得上:
- 加载到的配置与存储路径,例如
msg="storage ready" path=/var/lib/openviking/data; - 监听地址,例如
msg="http server listening" addr=0.0.0.0:8080; - 鉴权模式,例如
msg="auth enabled" mode=token。
如果只看到监听行、没有存储就绪行,先不要继续做隔离验证,说明数据落点还没确认。
写入两条不同隔离键的数据再交叉读回
隔离是不是真的生效,只能靠行为验证。先用同一个 token,写两条内容明显不同、隔离键不同的记录:
# 写入 A(项目 alpha)
curl -X POST http://127.0.0.1:8080/v1/memory \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tenant": "team1",
"project": "alpha",
"session": "s-001",
"content": "alpha 项目的发布流程是 A-B-C"
}'
# 写入 B(项目 beta,同样的 tenant)
curl -X POST http://127.0.0.1:8080/v1/memory \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tenant": "team1",
"project": "beta",
"session": "s-001",
"content": "beta 项目的发布流程是 X-Y-Z"
}'
接着做交叉读回:用 alpha 的隔离键去检索 beta 写进去的那句话,断言结果为空;反过来再用 beta 的键检索 alpha 的内容,同样应为空。同时用 alpha 的键检索 alpha 自己的内容,应当能命中。断言方式可以直接看返回体里是否包含对方那句原文,或者看结果条数是否为零,不要只看 HTTP 200 就认为通过。
# 交叉读回(期望:命中为空)
curl -X POST http://127.0.0.1:8080/v1/search \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"tenant":"team1","project":"alpha","session":"s-001",
"query":"beta 项目的发布流程"}'
如果这一条返回了 beta 的内容,排查顺序建议是:先看服务端有没有把三段拆开、有没有真的拼成过滤条件传下去;再看存储里那条记录的元数据是不是空的或写成了默认值;然后看检索请求经过中间层时隔离键有没有被丢掉;最后才怀疑索引本身。多数串数据是过滤条件没带上,而不是检索算法的问题。
在写入入口校验隔离键是否存在
空键写入是最常见的串数据来源:一个 Agent 忘了带 project,服务端又默认补成 default,两条本来无关的数据就落到了同一个命名空间。校验建议放在服务端的写入入口这一层,也就是请求解析之后、落存储之前,而不是只放在客户端 SDK 或网关:SDK 可以被绕过,而服务端代码里的校验更容易和存储路径对齐。
校验内容至少包括:三段是否都存在且非空、字符集是否符合命名规则、长度是否超限。拒绝写入时,返回明确的客户端错误(如 HTTP 400)并带上可定位的错误码与字段名,例如 {"error":"missing_scope_field","field":"project"}。不建议静默补默认值,也不建议返回 200 但实际没有写入。
拒绝写入的日志建议至少记录这些字段,方便事后对账:
request_id:与调用方日志对齐;tenant/project/session:原始值,缺失就记成空串,而不是省略字段;key_source:隔离键来自服务端身份推导还是请求体;decision与reason:reject 及具体原因;path:命中的写入接口,便于区分是哪个入口漏了校验。
这里有个边界需要自己权衡:严格校验会让调用方立刻失败,但配合明确的错误信息,通常比默默写进默认命名空间更容易排查。如果历史调用方暂时带不全字段,可以先在日志里统计缺失情况再逐步收紧,只是要清楚——在收紧之前,隔离并不成立。