Hermes 有记忆但没有外部语料检索。本指南建立这种连接——并证明它在模型无法伪造的语料库上有效。
这是 Hermes + DigitalOcean 系列的第 2 部分。第 1 部分:如何在 DigitalOcean 上运行 Hermes Agent。
大多数 RAG 教程从未证明检索确实发生了。他们搭建一个管道,向模型提问,得到正确答案,然后宣布胜利——而未检查模型是否仅凭通用知识就能回答该问题。如果能够,那么演示毫无意义。
本教程采取相反的方法。我们先编写测试问题 首先,验证模型在未附加检索时会失败(该失败记录作为我们的负对照),随后才构建管道并重新运行这些问题。到最后,您将获得三项成果:一个可用的检索设置、证明其有效的证据,以及一种可在任何 RAG 堆栈上复用的验证方法——而不仅限于此示例。
在任何其他说明之前,先澄清这一点,因为这是关于 Hermes 最常见的误解:Hermes 并不缺少记忆。 它随附代理策划的记忆文件、对过去会话的 FTS5 全文搜索、跨会话回忆以及技能。如果您按照第 1 部分操作,您已经看到它能够跨对话记住事物。Hermes 并未随附的是 您自有外部语料的检索 —— 您的运行手册、内部文档、产品知识。记忆涉及对话。检索涉及您的文档。它们是不同的问题,而本教程解决了第二个问题。
我们将构建的具体用例:一个值班代理,能够从您实际的运行手册中回答“错误 FM-4419 的含义是什么,我应该呼叫谁?”,而不是仅根据错误的形状进行模式匹配。
您将获得:
一切 — 示例语料库、插件源代码和测试问题 — 都位于 配套仓库,因此您可以跟随学习或直接跳过。
范围说明:此设置旨在在可用语料库上实现检索的 correct。将其扩展到数百万份文档 — — 高吞吐嵌入、负载下的检索延迟 — — 是另一个架构问题,也是另一篇文章。
genai:read 权限的 DigitalOcean 个人访问令牌doctl 已安装并经过身份验证本教程使用的版本: Hermes Agent v0.20.4 (2026.8.18),模型 slug 为 deepseek-v4-pro-0813 和 qwen3.8-max 在 DigitalOcean 无服务器推理上。Hermes 更新很快,DigitalOcean 会淘汰旧的模型 slug,因此如果下面的命令失败,请先查看 故障排除部分,再假设教程有问题。
以下三种看似解决方案的方法,以及它们为什么不行:
上下文窗口无法解决此问题。 您不能在每轮对话中粘贴每份运行手册。即使语料库从技术上可以容纳,塞入上下文也会降低成本并削弱模型对重要部分的注意力。
网页搜索无法解决此问题。 您的运行手册不在网络上。如果它们在网络上,那么您就有其他问题。
Hermes 自身的记忆无法解决此问题。 记忆会回忆 您的对话。它会记得您上周二讨论了 FM-4419。如果没有人告诉它 FM-4419 的含义,它就不会知道。
您真正需要的是一个能够自行判断何时需要调用语料库的代理,仅获取相关的文本块,并准确告知您这些块来源于哪些文档。这就是我们正在构建的循环。
完整的链条如下所示:
Spaces (source docs)
→ Knowledge Base (chunk + embed + index)
→ Retrieve API (kbaas.do-ai.run) ← the Hermes plugin calls this
→ Hermes agent loop
→ DigitalOcean Serverless Inference (the model)
在你构建任何东西之前值得做的修正:无服务器推理并不知道知识库的存在。 检索和完成是两个完全独立的 API 调用。如果你的心智模型是“我会把我的推理端点指向我的知识库”,你就会撞墙,因为根本没有东西可指向。本教程中我们构建的插件就是将它们连接起来的东西:它从 Retrieve API 获取数据块,并将其放入代理的上下文中,随后代理的下一次推理调用会基于这些数据块进行推理。
在以下情况下使用 Retrieve API + 插件方法(本教程):
alpha、结果数量、格式)在代码中确定性地固定改用托管的替代方案 — 将知识库附加到代理并使用 include_retrieval_info 调用其端点 — 何时:
在 MCP 服务器处停止(选项 A,下面)何时:
值得提出本教程跳过的前置问题:使用托管的 Knowledge Base,还是自建向量存储?自己运行 Qdrant、pgvector 或 Pinecone 可以获得对索引的控制——你可以选择嵌入模型、自行分块和实现混合搜索、跨云迁移——以此换取对摄取管道及其背后数据库的所有权;而 DigitalOcean Knowledge Base 会自动处理分块、嵌入、混合检索以及可选的重排序,并在你未提供时自动配置和调整底层 OpenSearch 数据库的规模。我们在这里采用托管方案,因为目标是验证检索的正确性,而不是运营向量数据库——但下面的插件模式并不关心 HTTP 调用的另一端是什么,因此如果你已经在 DO 托管的 Postgres 上运行 pgvector,只需替换 URL,其余保持不变。
这是大多数 RAG 教程会跳过的部分,也是大多数 RAG 演示无法证明任何东西的原因。
设计原则:如果一个称职的模型能够凭借通用知识回答你的测试问题,那么你的测试就是毫无价值的。 向前沿模型提问 “HTTP 503 是什么意思,我应该检查什么?” 它会在不进行任何检索的情况下给出一个好答案。即使你的管道完全损坏,演示也仍然看起来完美。
因此,语料库必须是 不可猜测的。工作流程,按顺序如下:
这个三步法并不特定于 Hermes 或 DigitalOcean。它适用于任何 RAG 堆栈,并且是“我的演示产生了一个合理答案”与“我有证据表明检索确实发生了”之间的区别。
伴随仓库包含八个 Markdown 文件,记录了一个虚构的支付平台,该平台有四个服务:atlas-ingest、ledger-core、payouts-service 和 webhook-relay。语料库故意植入了模型无法知道的内容:
FM-4419 在此语料库中具有特定含义,而在其他地方则没有。Riverbend) 具有明确的范围——它覆盖哪些服务以及更重要的是它不覆盖哪些服务。payouts-service 的错误在整个语料库中出现,但升级文档明确指出它们不属于 Riverbend 值班轮换的责任。这用于测试模型是读取其检索到的内容,还是仅凭错误前缀进行自由联想。我们不会在这里粘贴所有八个文件——从 仓库 获取。结构比内容更重要:当你为自己的验证构建语料库时,窃取其中的 模式(不可猜测的细节、交叉引用、范围陷阱),而不是文件本身。
八份文档的语料库故意保持较小。这里的目标是证明正确性。将摄取和检索扩展到数百万份文档会引入不同的限制,并且值得单独处理。

创建一个 Spaces 存储桶并在文件夹前缀下上传语料库。类似 runbooks/ 的前缀效果很好。
前缀不仅仅是装饰。知识库会将每个文档的 file_id 存储为 {bucket}/{object_key} 的形式,因此统一的前缀(runbooks/)使你以后能够通过元数据过滤将检索范围限定在存储桶的子集上。如果你预计同一个存储桶中会有多个团队或文档类型,请现在就确定好前缀。

在控制面板:数据服务 → 知识库 → 创建。选择一个地区和嵌入模型,将你的 Spaces 存储桶作为数据源添加,然后开始索引。
在点击创建之前,你需要了解以下两点:
索引是一个会消耗 token 的计费作业。 每个文档都会被分块并嵌入,你需要为嵌入的 token 付费。对八个 Markdown 文件来说这微不足道;但对于真实的语料库,在索引之前你需要清楚自己要索引什么。这也不是即时的——即使是小型语料库,索引作业也可能需要几分钟。
分块策略是按数据源设置的,更改它意味着删除然后重新添加该数据源——这会触发完整的重新索引并产生新的嵌入费用。请提前慎重选择。基于章节的分块(按 Markdown 标题分割)和固定长度分块是可预测成本的选项,而基于章节的分块更适合已经具有有意义标题结构的运行手册式文档。
你在每次 API 调用时都需要知识库的 UUID,而在 GUI 中它并不显眼。可以在以下三个位置获取:
doctl gradient knowledge-base list现在导出它,以及你的令牌:
export DO_API_TOKEN="dop_v1_..." # PAT with genai:read — not a Spaces key
export KB_UUID="your-kb-uuid"
在使用 Hermes 前,先单独验证检索功能是否正常。若检索出现问题,最好现在就发现,而不是在调试包含其他四个移动部件的代理循环时才察觉。
最快的检查方法是打开知识库控制面板的 Retrieve 选项卡:使用 num_results: 5 和 alpha: 0.5 运行测试查询,即可看到返回的文本块及其相关性得分和来源元数据。

The Retrieve Endpoint 选项卡更进一步:它会根据当前知识库和设置生成一个可直接使用的 curl 命令。原始调用示例如下:
curl -X POST "https://kbaas.do-ai.run/v1/$KB_UUID/retrieve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $DO_API_TOKEN" \
-d '{
"query": "what does FM-4419 mean",
"num_results": 5,
"alpha": 0.5
}'
理解响应的结构很重要,因为插件依赖于它:
{
"results": [
{
"text_content": "FM-4419 indicates a single-shard lease expiry in ledger-core...",
"metadata": {
"item_name": "ledger-core-errors.md",
"page_number": 1
},
"score": 0.87
}
],
"total_results": 5
}
最重要的字段是 metadata.item_name。它是源文件名,也是后续引用的全部基础:如果它能从此响应通过插件进入模型的上下文,模型就能标注其来源。如果在格式化过程中丢失了该字段,无论怎样提示,引用都不可能实现。
关于 alpha,这个最不明显的参数:它设置了词法检索和语义检索之间的平衡。0 表示仅关键词,1 表示仅语义,介于之间的值会执行混合搜索。DigitalOcean 的指导是将混合作为默认,并从 0.5–0.7 范围开始 — 纯语义会偏离精确查询,纯关键词会错过同义词。
我们的测试问题涵盖了两个方向,这正是有趣的地方。“FM-4419 代表什么”是一个精确的标记查找,正是 DO 文档建议将 alpha 调整到 0 的产品代码查询类型。“如果它一直发生,我应该叫谁”是一种对话式查询,需要基于语义的匹配。一个能够同时处理这两种查询形式的语料库正是混合搜索所针对的场景,因此我们在插件中固定 alpha 为 0.5,而不是让模型在每次调用时自行选择。如果你的语料库主要由标识符组成 — 如错误码、SKU、工单号 — 请调低 alpha;如果是散文文本,则调高 alpha。
关键点:直接在此端点上运行测试问题。如果这里返回的不是正确的块,无论怎样进行 Agent 工程都无法解决 — 先修复语料库或分块策略,然后再继续。
有一条零代码路径和一条约 60 行代码的路径。先说老实话:零代码路径是可行的,有些读者应该直接采用它并停止。
config.yaml 注册 MCP 工具,无需涉及 Python。如果你在第 1 部分设置了 MCP 服务器,这一步骤与此相同:# ~/.hermes/config.yaml
mcp_servers:
digitalocean:
command: npx
args: ["-y", "@digitalocean/mcp"]
env:
DIGITALOCEAN_ACCESS_TOKEN: ${DO_API_TOKEN}
重启 Hermes,知识库工具将与其内置工具一起出现。这确实有效。如果您只需要默认的检索行为,且不需要控制参数或格式,请在此停止。
您为何仍要编写此插件 — — 以及为何它是您在生产环境中所需要的:
alpha 和 num_results 在您的代码中保持为常量,而不是每次调用由 LLM 选择。允许模型调节检索会导致运行不具确定性 — — 同一问题在不同运行中可能检索到不同的结果。MCP 是快速路径。插件是生产路径。本教程的其余部分将构建该插件。
插件由四个文件组成:
~/.hermes/plugins/do-knowledge-base/
├── plugin.yaml # manifest — what this is, what it needs
├── __init__.py # register() — wires schema to handler
├── schemas.py # what the LLM sees
└── tools.py # what runs
plugin.yaml:声明清单文件及所需凭据name: do-knowledge-base
version: 1.0.0
description: Retrieval over a DigitalOcean Knowledge Base
provides_tools: true
requires_env:
- name: DO_API_TOKEN
description: DigitalOcean personal access token with the genai:read scope
url: https://cloud.digitalocean.com/account/api/tokens
secret: true
- name: KB_UUID
description: UUID of the target Knowledge Base
url: https://cloud.digitalocean.com/gen-ai/knowledge-bases
值得突出的部分是 requires_env。在此声明您的凭据 — — 包含描述和 URL — — Hermes 会在安装时提示您输入这些凭据,并将其写入 ~/.hermes/.env。将令牌标记为 secret: true 会掩盖输入。这比在处理器中手动编写环境检查要好得多,也是使插件能够由非你本人的人安装的原因。
schemas.py: 为什么工具描述决定了模型何时调用您的工具description 字段是产品。它是模型在决定您的工具是否与当前问题相关时唯一读取的内容。描述模糊,工具未被使用。
KNOWLEDGE_BASE_RETRIEVAL_SCHEMA = {
"name": "knowledge_base_retrieval",
"description": (
"Search the team's operational runbooks for the payments platform " # names the domain
"(atlas-ingest, ledger-core, payouts-service, webhook-relay). "
"Use this for questions about error codes in the FM-#### format, " # names the query format
"escalation and on-call routing, service configuration, and "
"version-specific behavior changes. "
"Do NOT use this for general programming questions or anything " # negative space
"unrelated to the payments platform. "
"Always cite the source file names returned in the results." # citation instruction
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The question or search terms to retrieve documents for",
}
},
"required": ["query"],
},
}
每个条款都有其存在的理由:第一个命名领域,第二个命名错误代码格式(这样模型就能识别 FM-4419 为在范围内),第三个划定负空间(即 不 应该用该工具做什么),最后一个指示引用。
注意模式刻意不暴露的内容:alpha、num_results、过滤器。这些是 tools.py 中的常量,而不是模型可控参数。这是选项 B 中的确定性论点的具体体现。
tools.py: 处理程序以及如何为代理循环格式化检索结果来自 Hermes 插件文档的四条规则,每条都值得内化:
(args: dict, **kwargs) -> str**kwargs 以保持与未来 Hermes 版本的前向兼容性import json
import os
import urllib.request
_ALPHA = 0.5
_NUM_RESULTS = 5
_MAX_CHUNK_CHARS = 1200
_RETRIEVE_URL = "https://kbaas.do-ai.run/v1/{kb_uuid}/retrieve"
def knowledge_base_retrieval(args: dict, **kwargs) -> str:
try:
query = args["query"]
url = _RETRIEVE_URL.format(kb_uuid=os.environ["KB_UUID"])
payload = json.dumps({
"query": query,
"num_results": _NUM_RESULTS,
"alpha": _ALPHA,
}).encode()
req = urllib.request.Request(
url,
data=payload,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {os.environ['DO_API_TOKEN']}",
},
)
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.load(resp)
results = data.get("results", [])
if not results:
return json.dumps({
"results": [],
"note": "No relevant documents found in the knowledge base "
"for this query. Do not answer from general knowledge; "
"say the runbooks do not cover it."
})
formatted = [
{
"source": r.get("metadata", {}).get("item_name", "unknown"),
"page_number": r.get("metadata", {}).get("page_number"),
"excerpt": r.get("text_content", "")[:_MAX_CHUNK_CHARS],
}
for r in results
]
return json.dumps({"results": formatted})
except Exception as e:
return json.dumps({"error": f"knowledge base retrieval failed: {e}"})
有趣的部分不是 HTTP 调用 —— 而是格式化,因为格式化决定了引用的成败:
{source, page_number, excerpt}, 所以 item_name 会在模型的上下文中以一个无歧义的键存活下来。这是使“始终引用你的来源”变得可实现而非仅仅是愿望的管道。_MAX_CHUNK_CHARS 处截断。 这里的每个字符都会在每次检索轮次中被注入到提示中;这个常量是你的 token 成本杠杆。[] 会邀请模型用通用知识填补空白。明确的 “runbooks 不涵盖此内容” 指令则给它提供了可以说的话。__init__.py: 注册工具from .schemas import KNOWLEDGE_BASE_RETRIEVAL_SCHEMA
from .tools import knowledge_base_retrieval
def register(ctx):
ctx.register_tool(
"knowledge_base_retrieval",
"do-knowledge-base",
KNOWLEDGE_BASE_RETRIEVAL_SCHEMA,
knowledge_base_retrieval,
)
就这样。ctx.register_tool(name, toolset, schema, handler) 它把模型看到的 schema 绑定到实际运行的处理程序。
_ALPHA、_MAX_CHUNK_CHARS 和 num_results这三个常量被刻意放置在 tools.py 文件的顶部。它们共同控制答案质量以及每轮注入的 token 数量:更多的结果和更长的摘录会为模型提供更多素材,同时每次检索的成本也更高;alpha 参数在精确匹配和语义查询之间调节精度。它们值得根据语料库进行调优——但完整的优化矩阵超出了本文的讨论范围。对于运行手册式的语料库,上面的默认值已经很合理。
mkdir -p ~/.hermes/plugins/do-knowledge-base
# copy the four files into it
hermes plugins doctor ~/.hermes/plugins/do-knowledge-base --ci
hermes plugins enable do-knowledge-base
在启用之前运行 plugins doctor —— 它会实际演练发现、解析和注册路径,因而能在问题仍然易于修复时捕获结构性问题。随后 plugins enable 会触发清单中的 requires_env 提示,并将您的凭据写入 ~/.hermes/.env。
启动 Hermes 并检查:
hermes
/plugins
您应该看到:
✓ do-knowledge-base v1.0.0 (1 tools, 0 hooks)

此表格中的每一行都是我们在构建本教程时实际遇到的问题。
| Symptom | Cause | Fix |
|---|---|---|
| 插件未出现 | 插件需要手动启用 | hermes plugins enable ; 运行 HERMES_PLUGINS_DEBUG=1 hermes plugins list 以查看详情 |
| 插件未出现 | __init__.py 在下载过程中被重命名为 (init.py) |
将其改回原名 — Python 要求文件名必须完全匹配 |
| KB 返回 HTTP 401 | 令牌缺少 genai:read,或者您使用了 Spaces 密钥而非个人访问令牌(PAT) |
在控制台中检查令牌范围;Spaces 密钥和 PAT 是不同的凭据 |
| KB 返回 HTTP 401 | 凭据存储在 shell export 中,而不是 ~/.hermes/.env |
重新运行 plugins enable,或者直接将它们添加到 .env 并重启 |
no endpoints available: request rejected pre-queue |
模型 slug 错误或已被退役 — 不是插件问题 | doctl serverless-inference models list,然后使用 hermes model |
| 凭据已修改但未生效 | .env 仅在启动时读取 |
重启 Hermes |
其中一项值得单独说明。此 no endpoints available: request rejected pre-queue 错误看起来很令人担忧,且似乎与工具相关 — 您在安装插件后立即会看到它,并会认为插件导致了问题。表明这是提供商层面的问题,而非工具层面:它会在每次 every API 调用时触发,包括诸如会话标题生成之类的辅助操作。原因几乎总是已退役的模型 slug。DigitalOcean 会主动退役较旧的 slug,因此上个月仍然有效的配置可能会失效 — 每当出现此情况时,请使用 doctl serverless-inference models list 重新验证。
这是收获,也是我们一开始提出的方法的完成:先提问题,再做负面对照,最后运行管道。
payouts-service 错误,且升级文档明确将 payouts 放置在 Riverbend 的范围之外。依赖错误代码格式进行自由联想的模型会自信地将其路由错误。禁用插件后,我们提出了问题 3。代理搜索了其可用的表面 — — 包括本地文件系统 — — 在 FM-3350 或 Riverbend 值班轮换上未找到任何信息,并拒绝猜测:
“在事故中对‘谁应该被呼叫’做出错误猜测,比没有猜测还要糟糕。”
然后它询问知识库实际上位于哪里。

此记录演示了两点。首先,语料库设计有效:这些问题确实无法猜出,因此之后任何正确答案只能来自检索。其次 — — 值得单独指出 — — Hermes 诚实降级。面对它无法依据的问题,它拒绝回答并请求来源,而不是编造。这就是在随时待命的代理中所期望的失败模式。
启用插件后,同样的问题会产生可见的工具序列 — tool_search → tool_describe → knowledge_base_retrieval — 然后得到有依据的答案:
payments-riverbend Pageloop 键,而不是命名某个个体 — — 这正是 escalation 文档所规定的。
在这些答案中需要注意的细节是:每个都会命名其源文件。这就是 metadata.item_name 从 Retrieve API 幸存下来,经过插件的 {source, ...} 格式化,进入模型上下文。这里的引用不是提示技巧 — — 而是管道结果。如果插件在格式化过程中丢掉了 item_name,则 schema 中的任何指令都无法恢复它。
不是 “它回答正确了。” 通过条件有三部分:
该定义可适用于任何 RAG 堆栈。如果你的验证无法区分工作流程和知识丰富的模型,那就不是验证。
检索正常工作时,会出现一个诱人的优化想法:“检索到的块现在在承担主要工作——我会把这些轮次路由到一个更便宜的模型。”
DigitalOcean 自己的 推理路由器文档 建议不要在循环中途切换模型,原因有三,且这些原因会相互叠加:
正确的成本杠杆就是您插件中已有的检索参数。更少、更精准的块(_NUM_RESULTS、_MAX_CHUNK_CHARS、一个经过良好选择的 _ALPHA)会在每个轮次上缩小提示,且不会引入任何失败模式。为循环选择一个模型;调整您喂给它的内容。
在基础循环得到验证后,可以采取以下方向:
item_name 使用 starts_with,对 file_id 使用 wildcard — 这就是之前提到的 Spaces 文件夹前缀发挥作用的地方。将查询范围限定为共享语料库中某个团队的文件夹。kb_name 参数。超出小型语料库的规模 — 百万级文档、高吞吐嵌入、负载下的检索延迟 — 这是一个不同的架构问题,超出本文讨论范围。
链条:Spaces → Knowledge Base → Retrieve API → Hermes 插件 → DigitalOcean 无服务器推理。 实际工作:四个短文件,一个清单,一个 HTTP 调用。
此方法值得前沿推广:先编写不可猜测的测试问题,捕获负面对照,然后构建 — — 并且不要在工具禁用运行失败、工具启用运行带有引用的回答以及陷阱问题正确路由之前,称 RAG 管道已验证。
对 Hermes 用户而言,更广泛的观点是插件界面是连接 Hermes 与 X 的通用答案。“如何将 Hermes 连接到 X” — 而今天的 X 是知识库。
资源:
#plugins-skills-and-skins不。Hermes 自带记忆功能 — — 包含代理 curated 的记忆文件、过去会话的全文搜索、跨会话回忆 — — 但记忆仅覆盖您的对话,而不包括您拥有的外部文档语料库。在您自己的文档上进行检索需要将 Hermes 连接到向量存储或托管的知识库,无论是通过 DigitalOcean MCP 服务器,还是通过类似上面构建的自定义插件。
不。DigitalOcean 知识库负责分块、嵌入、混合检索以及可选的重排序,并为您准备好底层的 OpenSearch 数据库。自行运行 Qdrant、pgvector 或 Pinecone 可以获得对索引的更多控制权,但需要您自己管理摄取管道和数据库 — — 本教程中的插件模式适用于任意一种,因为它仅仅是一个 HTTP 调用。
针对模型凭借通用知识无法回答的语料库进行测试。先编写问题,在检索功能关闭的情况下运行并确认模型无法回答,然后打开检索功能重新运行。当工具禁用的运行被拒答、工具启用的运行不仅正确回答 并且 引用其来源,且一个精心设计的陷阱问题能被正确路由而非仅依赖模式匹配时,该管道即被视为有效。
alpha 设置为多少?The alpha parameter sets the balance between keyword and semantic retrieval: 0 is keyword-only, 1 is semantic-only. DigitalOcean 建议 hybrid as the default, starting in the 0.5–0.7 range. Tune lower for corpora dominated by exact identifiers like error codes or SKUs, and higher for conversational prose.
不要在循环中途切换。不同模型的工具调用格式不同,切换可能会导致你的代理解析工具调用出错,而且模型切换会使基于前缀的 KV 缓存失效——这会增加你本来想优化的事物的成本。相反,应减少检索参数:使用更少的块和更短的摘录可以在每轮中缩小提示,且不会引入故障模式。
——
一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。