科研工作流的最后一公里是"成稿"——把一堆分析结果、笔记、引用,拼成一份格式规范的 Word 或 PDF。这一步最磨人的不是写内容,而是排版、对齐引用、统一格式。Canguo Science 把它封装成了"学术 Skills":模型负责产出内容,Skill 负责把内容变成规范文档。

这篇讲讲这条链路在工程上怎么落地,用能跑的 Python:让模型返回结构化输出 → 用 python-docx 生成规范 Word → 处理字体 / 表格 / 引用 → 导出 PDF。模型这一层在 Canguo Science 里统一走 CanguoAI 的 OpenAI 兼容入口,所以下面的代码换任意模型都一样,不影响成稿逻辑。代码为便于阅读做了简化。

一、先让模型给你"结构化"的输出

直接把模型吐的一大段 Markdown 硬塞进文档,格式很难控制。更稳的做法是让模型返回结构化 JSON,成稿逻辑只跟固定的数据结构打交道:


 

import os, json

from openai import OpenAI

client = OpenAI(

api_key=os.environ["LLM_API_KEY"],

base_url=os.environ["LLM_BASE_URL"], # OpenAI 兼容入口的 /v1,换模型只改 model

)

SCHEMA_HINT = """

只输出 JSON,结构为:

{"title": "标题",

"sections": [{"heading": "小节标题", "body": "正文"}],

"references": ["文献1", "文献2"]}

"""

resp = client.chat.completions.create(

model="gpt-4o-mini",

messages=[

{"role": "system", "content": "你是科研写作助手。" + SCHEMA_HINT},

{"role": "user", "content": "写一份关于扩散模型的简短综述"},

],

response_format={"type": "json_object"}, # 保证输出是合法 JSON

)

data = json.loads(resp.choices[0].message.content)

一个细节:用 response_format={"type": "json_object"} 时,prompt 里必须出现 "JSON" 字样,否则接口会报错——这是 OpenAI 兼容协议的约定。

二、用 python-docx 把结构化数据变成 Word

拿到规整的 data 之后,生成文档就是"照着结构填":


 

from docx import Document

def build_docx(data: dict, out="report.docx"):

doc = Document()

doc.add_heading(data["title"], level=0)

for sec in data["sections"]:

doc.add_heading(sec["heading"], level=1)

doc.add_paragraph(sec["body"])

if data.get("references"):

doc.add_heading("参考文献", level=1)

for i, ref in enumerate(data["references"], 1):

doc.add_paragraph(f"[{i}] {ref}")

doc.save(out)

build_docx(data)

结构化的好处在这一步体现得淋漓尽致:模型输出一变,文档跟着变,中间没有脆弱的字符串解析。

三、中文字体:一个绕不过去的坑

python-docx 设中文字体有个经典陷阱——只设 font.name 对中文不生效,中文会回退成默认字体。正确做法是额外通过 w:eastAsia 设一遍


 

from docx.shared import Pt

from docx.oxml.ns import qn

def set_base_font(doc, latin="Times New Roman", cjk="宋体", size=12):

style = doc.styles["Normal"]

style.font.name = latin

style.font.size = Pt(size)

# 关键:中文字体必须单独通过 w:eastAsia 设置,否则不生效

style.element.rPr.rFonts.set(qn("w:eastAsia"), cjk)

这个坑几乎每个用 python-docx 排中文文档的人都会踩一次,记住 qn("w:eastAsia") 就行。

四、表格与图表:把实验结果放进去

科研文档少不了表格和图。表格用 add_table,图用 add_picture


 

from docx.shared import Inches

from docx.enum.text import WD_ALIGN_PARAGRAPH

def add_table(doc, headers, rows):

table = doc.add_table(rows=1, cols=len(headers))

table.style = "Table Grid" # 带边框的内置样式,最稳

for j, h in enumerate(headers):

table.rows[0].cells[j].text = str(h)

for row in rows:

cells = table.add_row().cells

for j, v in enumerate(row):

cells[j].text = str(v)

def add_figure(doc, img_path, caption=None):

doc.add_picture(img_path, width=Inches(5.5))

if caption:

p = doc.add_paragraph(caption)

p.alignment = WD_ALIGN_PARAGRAPH.CENTER

add_table(doc=Document(), headers=["方法", "准确率"],

rows=[["A", "0.91"], ["B", "0.88"]])

Table Grid 是内置样式、任何环境都有,比那些花哨样式名稳;图的宽度用 Inches / Cm 控制,避免原图过大撑破版面。

五、引用与编号:让正文和参考文献对得上

综述里最容易出错的是引用编号——正文里的 [3] 得和文末第 3 条对得上。与其手动数,不如维护一个引用登记表,自动分配编号:


 

class Citations:

def __init__(self):

self._refs = []

self._index = {}

def cite(self, ref: str) -> str:

"""引用一次,返回对应编号(同一文献编号固定)。"""

if ref not in self._index:

self._refs.append(ref)

self._index[ref] = len(self._refs)

return f"[{self._index[ref]}]"

def bibliography(self) -> list[str]:

return [f"[{i}] {r}" for i, r in enumerate(self._refs, 1)]

cite = Citations()

para = f"扩散模型最早由相关工作提出 {cite.cite('Ho et al., 2020')}," \

f"后续被大量改进 {cite.cite('Song et al., 2021')}。"

print(para) # ...提出 [1],后续被大量改进 [2]。

print(cite.bibliography()) # ['[1] Ho et al., 2020', '[2] Song et al., 2021']

正文按出现顺序自动编号,文末一次性生成参考文献列表,两边永远对得上——这类机械又易错的活,正是"学术 Skill"最该替你干的。

六、导出 PDF:别指望纯 Python 完美转换

很多人以为有个库能把 docx 一键完美转 PDF,实际没有那么理想。几条现实路径:

  • docx2pdf:效果好,但依赖本机装了 Word(Windows / macOS);
  • LibreOffice headless:服务器首选,命令行批量转,无需 Word;
  • reportlab:从结构化数据直接另画 PDF,排版完全可控,但样式得自己写。

服务器环境推荐 LibreOffice,一行命令搞定:


 

import subprocess

def docx_to_pdf(path, outdir="."):

subprocess.run(

["soffice", "--headless", "--convert-to", "pdf", "--outdir", outdir, path],

check=True,

)

选哪条看场景:要还原 Word 版式用前两个,要完全掌控排版、且不怕多写代码就用 reportlab。

七、成稿也要可溯源

最后补一句和前面工作流的衔接:Canguo Science 里每段内容都能挂上生成它的 run_id。成稿时把这个信息留在段落或脚注里,读者(或审稿人)就能顺着它回溯到原始运行——引用不是"编"出来的,而是从真实运行档案里回溯出来的。这是把"可溯源"一路贯穿到最终文档的关键一步。

小结

把模型输出变成规范文档,工程上就三件事:

  • 先要结构化输出(JSON),别让脆弱的字符串解析毁掉排版;
  • python-docx 的中文字体要设 w:eastAsia,表格用 Table Grid 最稳;
  • 引用自动编号,正文和文末靠登记表对齐;
  • PDF 转换按场景选,服务器用 LibreOffice headless。

这些拼起来,就是"从模型输出到一份能交付的 Word / PDF"这条链路。它跟你用哪个模型无关——在 Canguo Science 里模型走 CanguoAI 的统一入口接进来,成稿逻辑一行都不用改。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐