对文档的回答只能和构建它的文本一样好,而当你在聊天中粘贴整个文件时,你无法判断模型实际使用了哪一部分。GroupDocs.Search.Mcp 在本地 Docker 中运行,为 Claude Desktop、Cursor 或 Copilot 提供了一种不同的原语:get_text_fragments 只返回匹配查询的片段,并附带少量上下文以及来源文件名,从而让代理直接引用答案:

我的合同中关于罚金的规定是什么?请引用相应的段落,并提供足够的上下文以便理解每一段。

带有配置和故障排除的逐步说明请参见文档:How to get the matching passages instead of reading whole files。

为什么答案会偏离文档?

因为 context window 是一种预算,而整篇文档会大量消耗它。关于罚金的问题可能只需要五十页中的三句话。粘贴全部五十页后,模型必须自己在所有内容中找到这三句话,并在写作时保持它们。长上下文会被摘要化,前面的材料会失去权重,答案就会依赖模型对合同通常说法的整体感知,而不是你自己的合同。业界将这种管理方式称为 context engineering;最简单的做法就是不加载不需要的文本。

片段是什么样的?

索引已经保存了每个文档的全部文本,服务器可以截取每个匹配项周围的片段。这些是我们对五份合成合同进行测试时得到的真实响应。默认情况下,前后各八个词会得到一个短语:

-> get_text_fragments {"query":"penalty","indexName":"contracts"}
<- === contracts/acme-master-services-agreement.pdf (1 occurrence(s)) ===
     [content] …before the end of the Term, a termination penalty of
   three monthly fees applies, payable within 30…

前后各二十个词则会得到完整条款:

-> get_text_fragments {"query":"penalty","indexName":"contracts",
     "termsBefore":20,"termsAfter":20,"maxFragmentsPerDocument":1}
<- === contracts/acme-master-services-agreement.pdf (1 occurrence(s)) ===
     …convenience on 90 days written notice.
   12.3 If the Client terminates before the end of the Term, a termination
   penalty of three monthly fees applies, payable within 30 days of the
   termination date.
   14.2 Force majeure: neither party is…

termsBefore 和 termsAfter 是一个调节器:设小一点用于“哪些文件以及大致位置”,设大一点用于“引用完整条款”。maxFragmentsPerDocument 与 maxDocuments 用来限制答案的大小,这样出现两百次的词也不会淹没上下文。

预算对比

Reading the files                        Searching the index
-------------------------------------    -------------------------------------
agent opens every document               search: which files match (JSON)
  context grows with the corpus          get_text_fragments: the passages
  long files crowd out the question        (~8 words either side, per file)
  repeated for every question            answer cites file + passage
                                         the index is reused next time

差距随语料库增大而扩大。对三个短文件几乎没有影响,但对包含长合同、报告和邮件的文件夹来说,单个问题的片段可能有几百个词,而完整的候选文档文本则要多得多。

当词语不同

全文搜索匹配词语,这既是实现 grounding(每个命中都有解释)的特性,也是召回率的限制(同义词会被漏掉)。代理的任务是尝试变体,并且它可以看到每个变体实际匹配了什么:

-> search {"query":"renewal OR renews","indexName":"contracts"}
<- umbrella-office-lease.pdf, terms ["renewal", "renews"]

-> search {"query":"renewl","indexName":"contracts","fuzzy":true}
<- umbrella-office-lease.pdf, terms ["renewal", "renews", "rent"]

模糊匹配拯救了拼写错误,同时也把 rent 拉了进来。因为响应列出了匹配的词,代理或你都可以看到“过度匹配”,而不是盲目信任。这种透明度正是“grounded”应有的含义:不仅“答案引用了来源”,而且“你可以看到来源是如何被找到的”。

一个 grounded 答案的示例

请求引用时,答案会以一目了然的形式呈现:

两份合同包含终止罚金。Acme MSA,第 12.3 条:“a termination penalty of three monthly fees applies, payable within 30 days of the termination date.”。Globex 咨询协议,第 9 节:“the Client pays 50 percent of the fees remaining for the current phase.”。Initech NDA 提及终止和罚金,但声明没有罚金。

如果摘要误读了引用,误读会紧挨着引用出现。

诚实的限制

  • 在 evaluation mode 下,索引最多容纳 100 份文档,搜索最多返回 10 条。评估模式还会在某些格式的索引文本中添加 “Evaluation only…” 行,这可能出现在片段中;请为服务器授权并重新构建索引以去除该行。先调用 get_license_status。
  • 片段的上下文是按词计数的,而不是按句计数,因此片段可能从句中间开始;在需要时提升 termsBefore。
  • 不支持 OCR:没有文本层的扫描件没有可返回的片段。
  • 仅限 Docker;在 Linux 上请添加 --user "$(id -u):$(id -g)"。

FAQ

如何让 Claude 引用我的文档来源?
为它提供一个返回源文本及文件名的工具。get_text_fragments 正好做到这一点,配合 “quote the passage and its file” 之类的指令即可形成习惯。

这会降低 token 使用量吗?
它避免了加载整篇文档来回答问题:代理只读取匹配的片段。节省多少取决于文件的大小。

代理在需要时还能读取整篇文件吗?
可以,使用它自己的文件工具。片段用于查找和引用;当片段不足时再读取整篇文件。

深入了解