让 AI 代理“为 Contoso 编写服务协议”,它会用自己的语言、自己的标题,并自行计算出总额。这不是正确的分工。使用本地运行的 GroupDocs.Assembly.Mcp(dnx GroupDocs.Assembly.Mcp --yes),Claude Desktop、Cursor 或 Copilot 会询问你的 Word 模板需要哪些字段,从你的笔记中填充这些字段,随后引擎渲染合同、表格和算术运算,模型不接触布局:

为此订单使用 contract-template.docx 制作服务协议并保存为 PDF:Contoso Pharma,Keizersgracht 210 Amsterdam,联系人 Dr. Lena Brandt;12 个月托管服务,费用 4,800 欧元,12 个月支持,费用 1,200 欧元,一次入职培训,费用 3,500 欧元;5% 折扣。

带有配置和故障排除的逐步版本请参见文档:How to generate documents from templates with AI agents using MCP.

为什么模型不应该直接编写文档?

因为生成的文档会以难以察觉的方式出现错误。布局会偏离法律批准的版本。条款被改写成看起来通顺但实际含义不同的内容。总额是通过下一个 token 的预测得出,而不是通过乘法计算。而且每次运行都会略有差异,上个月的审查对本月的合同毫无参考价值。

模板则颠倒了这种情况。法律部门拥有 contract-template.docx,其中包含标题、签名块以及 <<[customer.Name]>> 标记。模型的工作仅限于它擅长的部分:读取电子邮件或一组笔记并将其转换为结构化值。引擎完成其余工作,每次都以相同方式执行。

代理如何知道要填充什么?

它会询问模板。get_template_fields 在不运行引擎的情况下读取标签,并返回模板使用的名称、它读取的每个字段以及用于填充的 JSON 骨架。这是服务器为我们的合同模板返回的内容:

-> get_template_fields {"template":{"filePath":"contract-template.docx"}}
<- { "tagCount": 17,
     "dataSourceNames": [ "contract", "customer", "items" ],
     "dataSkeleton": {
       "contract": { "Number": "", "Date": "", "Discount": "" },
       "customer": { "Name": "", "Address": "", "Contact": "",
                     "Email": "" },
       "items": [ { "Title": "", "Quantity": 0, "UnitPrice": 0 } ] } }

该骨架是模型与文档之间的契约。代理从订单文本中填充它,并以 dataJson 的形式返回。

当代理忘记某个字段时会怎样?

系统会精确地告知它。在我们的测试中,代理的第一次尝试遗漏了客户的电子邮件地址。服务器拒绝并指出了缺口:

-> assemble_document {"template":{...},"dataJson":"{... no Email ...}"}
<- Document assembly failed for 'contract-template.docx': ...
   Can not get the value of member 'Email' on type ''. The template
   reads 'Email', which the data does not have. Check the spelling
   (names are case-sensitive) with get_template_fields and add it to
   the data. ... pass allowMissingMembers=true to output nothing for
   missing members and empty lists.

模型直接写自由文本的话可能会悄悄地遗漏该行。在这里,代理可以向你询问地址,或与你一起决定该字段可以保持为空。

完整交互过程

user            agent                       Assembly MCP server
 | "Make the contract for this order: ..."                      |
 |-------------->|                                              |
 |               |-- get_template_fields {"template":          |
 |               |     "contract-template.docx"} ------------->|
 |               |<- dataSkeleton: contract, customer, items --|
 |               |   (fills the skeleton from the order text)  |
 |               |-- assemble_document {"dataJson":"{...}",    |
 |               |     "format":"pdf"} ----------------------->|
 |               |<- Saved to: .../contract-template_          |
 |               |   assembled.pdf ----------------------------|
 |<- "Contract ready: 3 items, total 75500 EUR, 5% discount."  |
   layout, table and arithmetic come from the template

在添加电子邮件后,第二次调用成功:

-> assemble_document {"template":{"filePath":"contract-template.docx"},
     "dataJson":"{...}","format":"pdf"}
<- Assembled 'contract-template.docx' with inline JSON (JSON object)
   as PDF
   Saved to: .../contract-template_assembled.pdf

文档包含一个由表格行中的 <<foreach>> 构建的三行费用表,金额由模板计算(<<[item.Quantity * item.UnitPrice]>>),总计 75500 欧元来源于 items.Sum(...),以及一行 “适用 5% 折扣。”,该行仅因 <<if>> 标记在数据中发现折扣而出现。模型并未计算这些内容。

这比托管模板服务更好吗?

这是一种不同的权衡。托管模板服务通过 HTTPS 接收你的模板和数据,在其云端渲染并按文档或按积分收费。而这里,模板、数据和输出都保存在本机的文件夹中;引擎是运行在进程内的 GroupDocs.Assembly,且没有任何端口在监听。传输给模型提供商的只有你粘贴的订单文本和代理填充的骨架,而不是渲染后的合同。相关架构请参见关于 on-premise AI document processing 的系列文章。

诚实的限制

  • 评估模式:每个生成的文档都会以 “Evaluation Only. Created with GroupDocs.Assembly 26.7…” 开头,保存的工作簿会额外增加一个工作表,电子邮件模板仅保留纯文本正文。使用许可证文件或计量密钥可去除上述三项。
  • 模板语法为 <<[name]>>。{{name}} 占位符会保留为文本;get_template_fields 会说明这一点。
  • 模板被视为可信输入:图像和文档标签会加载其表达式返回的任意路径或 URL。为服务器挂载一个文件夹即可。
  • 日期:以 2026-11-12T00:00:00 的形式编写,并在模板中进行格式化(<<[contract.Date]:"d MMMM yyyy">>)。仅包含日期的 "2026-11-12" 在我们测试的 UTC+2 机器上会显示为 11 November。
  • 代理本身即为数据源。让它在渲染之前列出提取的值,并进行审查。

常见问题

AI 代理能在不上传的情况下填充我的 Word 模板吗?
可以。服务器本地通过 stdio 运行,并从你配置的文件夹读取模板;输出文件会写在同一目录下。

模板可以是什么格式?
Word、Excel、PowerPoint、Markdown、HTML、文本以及电子邮件模板;每种模板都可以保存为其对应格式或 PDF 等其他格式。get_supported_formats 会列出每种模板支持的格式。

如果模板由他人维护怎么办?
这正是目的所在。模板的所有者可以更改条款和布局;代理仍然填充相同的骨架,而 get_template_fields 会在下次询问时显示任何新字段。

深入了解