文字只是简单的部分:为 AI 生成文章配图的完整流水线
生成文字已经不是问题,为文章配图才是。这是一条端到端的流水线——路由提示词、照片搜索、Mermaid 图表、经过核实的图表,以及把它变成 SEO 资产的 alt 文本、署名和 ImageObject 标记。
你是一名有内容产出目标的开发者:每月几十篇文章,也许上百篇。写作模型负责草稿、大纲、元描述、内部链接。然后流水线撞上了唯一没有专属模型的一步——图片——然后停了下来。不是因为图片难找,而是因为整个技术栈里没有任何一环知道该用哪张图片、来自何处、拥有什么许可、该怎样描述。
本文正是这缺失的一步,端到端地打通:针对哪种段落该生产哪种视觉内容、把成稿转化为搜索简报的提示词、返回照片的 API 调用、绘制照片无法表达内容的第二个模型,以及输出 Google 真正能读懂的标记的组装器。文末附两套完整的工作流,可直接照搬使用。
文本已被解决。插图才是卡壳之处。
对自动化文章流水线做一次诚实的审计。大纲:已解决。草稿:已解决。 标题、元描述、结构化数据、内部链接、翻译:已解决,全部由同一个模型、以文本形式完成。然后:
| 流水线步骤 | 状态 | 实际卡点 |
|---|---|---|
| 大纲与草稿 | 已解决 | 一次模型调用,一个提示词 |
| 标题、元描述、结构化数据、链接 | 已解决 | 文本输入,文本输出 |
| 首图 | 卡住 | 需要真实文件、许可、尺寸和 alt 文本——这些都不是文本模型能产出的 |
| 段落配图 | 卡住 | 每篇文章三到五张,各不相同,且不能与站内其他文章重复 |
| 图表与示意图 | 卡住 | 必须准确——是唯一一种搜索无法返回、生成器也绝不能凭空捏造的视觉内容 |
这种失败不是审美问题,而是结构性问题:文章最终配上了库存占位图,或者与前十二篇用了同一张照片,又或者配了一张六指手的生成图像,成了读者第一眼看到的东西。而图片并不只是装饰——Google 自己的图片文档说得很直白:alt 文本“是为图片提供元数据时最重要的属性”,其指导原则是使用真实的 <img> 元素并配以描述性 alt,而不是使用 CSS 背景图,这样图片才能被发现、被理解。1
四类视觉内容,四类模型
最大的设计错误,是把“配图”当作只有一个提供者的单一问题。实际上它是四个问题,而它们之间的路由只是一行提示词,不是一项服务:
| 该段落需要…… | 用它来生成 | 为什么不能用其他方式 |
|---|---|---|
| 一个真实世界的场景首图、人物情境、地点、物品、动作 | 语义化照片搜索(Pexafy) | 生成器会捏造细节;图表则无内容可绘 |
| 你手头已有的数字基准测试、定价、调查结果、延迟 | 由模型编写绘图代码,在沙箱中运行 | 图像模型不能被信任去处理数值;照片无法承载数据 |
| 一种结构或流程架构、时序、状态机 | 由模型编写 Mermaid / Graphviz,确定性渲染 | 免费图库中没有你的系统的示意图 |
| 你产品的界面截图文档、更新日志、教程 | 脚本化浏览器截图(Playwright) | 没有别的东西能展示只存在于你自己构建版本中的界面 |
那生成插图呢?它保留了一个诚实的用途:无法拍摄、也不是数据的场景——一个抽象机制、一个尚不存在的产品、一种你拥有版权的房屋插图风格。(关于真实照片胜过生成图像的完整论证——批量下的速度、准确性、同质化问题—— 已在这里给出。) 价格随趋势变化:自2026年8月2日起, 欧盟人工智能法案第50条要求生成式系统的提供者以机器可读格式标记合成输出。2这是对AI 提供者和部署者的义务,不是关于博客可以发布什么的规则——但这正是文章顶部图片的来源正日益成为读者可以核实、而非仅凭信任接受的原因。
端到端的流水线
五个阶段。只有第 3 阶段调用图片 API,只有第 4 阶段是可选的:
┌─ 1. WRITE ────────────────────────────────────────────────────┐
topic ──▶ LLM ──▶ draft.md (h2 sections, front-matter)
└───────────────────────────────┬───────────────────────────────┘
┌─ 2. BRIEF ────────────────────┴───────────────────────────────┐
draft.md ──▶ LLM ──▶ { hero: {...}, sections: [ {...} ] }
one JSON object: per slot, a kind +
either a camera brief or a data/diagram spec
└──────────┬──────────────────────────────────┬─────────────────┘
│ kind = "photo" │ kind = "chart" | "diagram"
▼ ▼
┌─ 3. SEARCH ───────────────┐ ┌─ 4. DRAW (optional) ─────────┐
GET /search/photos LLM ──▶ mermaid | plotting code
← credited photo + ──▶ sandbox ──▶ .svg / .png
w/h, blur_hash, alt, (deterministic render, no
licence, source URL invented numbers)
└──────────┬────────────────┘ └──────────────┬───────────────┘
└───────────────┬──────────────────┘
┌─ 5. ASSEMBLE ─────────────┴───────────────────────────────────┐
<img> with width/height + fetchpriority | loading
alt written for a human · visible credit · ImageObject JSON-LD
photo_id stored so no two pages share a hero
└───────────────────────────────────────────────────────────────┘
比这张示意图更重要的是两条特性。阶段 2 是一个路由器:它为每个位置决定由哪个生产者来处理,因此你绝不会向图片库索要一张柱状图。而阶段 5 正是 SEO 真正发生的地方——Google 文档中所有关于图片的要求(描述性 alt、许可元数据、LCP 安全的首图)都在这里输出,其字段全部来自搜索响应中已经携带的数据。
阶段 2:把草稿转化为视觉简报
对成稿进行一次模型调用,它返回的是一份方案,而不是一条查询。 如何撰写单张照片简报——为什么文章标题是最糟糕的输入,一份12到25词的取景简报是什么样子,以及它可能出错的方式——已在 单篇文章配图指南中完整给出,这里不再重复。流水线额外增加的是 路由:同一次调用必须逐个位置决定由哪个生成器来处理——图表条目携带数据,而照片条目携带场景。
# system prompt — run once per finished draft
You are the art director of a technical publication. Read the article and
return the visual plan: one entry for the hero, one per H2 section.
For each entry choose exactly one kind:
"photo" a real scene: someone doing something somewhere, a place,
an object, a gesture. The default for heroes.
"chart" the section states numbers that are IN the article. Never
invent values: copy them into data, verbatim.
"diagram" the section describes a structure, a flow or a sequence.
"none" the section is short, or already carries a code block.
Rules for "photo" entries — the field is query:
撰写一段镶头简介:描述一个相机可以拍摄到的场景,用英文写成,字数在12到25词之间,
并与该章节的氛围相符。要写出画面中出现的实际事物,而不是主题本身。禁止出现文字、
徽标、品牌或名人;也不要使用看不见的隐喻。(完整规则,含示例与失败案例:
pexafy.com/blog/illustrate-blog-articles-at-scale/)
Do NOT write the alt text of a photo entry: the picture you get back is the
closest match to the brief, not the scene you described, so its alt has to be
written from the chosen photo. Chart and diagram entries DO carry an alt —
there you control exactly what is rendered.
Return JSON only:
{
"hero": { "kind": "photo", "query": "…", "orientation": "landscape" },
"sections": [
{ "h2": "…", "kind": "photo", "query": "…" },
{ "h2": "…", "kind": "chart", "title": "…", "unit": "ms",
"data": [ {"label": "…", "value": 0} ], "alt": "…" },
{ "h2": "…", "kind": "diagram", "spec": "flowchart LR; …", "alt": "…" }
]
}
kind这一行完成了大部分工作,data指令则完成剩余部分:图表条目只能携带草稿中已经出现的数字,因此模型是在转录而非虚构。以下是路由器在一篇开发者文章的三个真实段落上的运行结果:
| 段落 | kind | 路由器返回的内容 |
|---|---|---|
| 首图 —“为什么我们的夜间构建要花 40 分钟” | photo |
“一名开发者深夜在桌前工作,配有两台显示器和一个机械键盘,房间昏暗,仅靠屏幕光照亮” |
| “时间究竟花在哪里” | chart |
从段落中复制的 data:安装 480 秒,编译 1080 秒,测试 720 秒,上传 120 秒 |
| “我们如何拆分构建图” | diagram |
任务依赖图的 flowchart LR |
| “我们做了哪些改动,哪些还会再做一次” | photo |
“两名工程师站在写满示意图的白板前,一起解决一个问题” |
阶段 3:带署名的照片被返回
每个 kind: "photo" 条目对应一次请求。上面的首图简报,在公开 API 上运行,
在 147 毫秒内返回了以下结果:
最后一段的简报,是一个完全不同的场景,耗时 144 毫秒:
每张照片返回的内容,正是让阶段 5 得以实现的关键——不仅仅是一个文件:
{
"photo_id": "019e1c7f-0063-759e-b498-33ce1714e6c9", // 保存它:不重复使用
"urls": { "small": "…?w=400", "regular": "…?w=1080",
"large": "…?w=1920" },
"width": 3000, "height": 1688, // → 无布局偏移
"blur_hash": "LJ8gjv9rVq-6OFxanNNFI7xco$Na", // → 真实占位图
"alt_description": "Person types on keyboard in front of dual monitors…",
"photographer_full_name": "Jakub Żerdzicki", // → ImageObject.creator
"source": "Unsplash", "license_type": "free",
"source_image_url": "https://unsplash.com/photos/…",
"attribution": { "html": "<span…>Photo by …</span>",
"plain": "Photo by Jakub Żerdzicki on Unsplash (…)" }
}
阶段 4:照片无法表达的内容
计划中有两个位置是无法搜索得到的,而这正是第二个模型发挥作用的地方——不是去画一张图,而是编写画图的代码。这个区别很重要:代码是可审查的、确定性的,也不可能凭空幻想出一个柱子的高度。
示意图:文本输入,SVG 输出
Mermaid 能从纯文本定义中渲染出示意图,3这使它成为交给模型最安全的目标:输出内容可检查、可在 git 中做差异对比,并且每次渲染结果一致。路由器已经返回了对应的 spec。
# the "spec" field of a diagram entry, written to build/graph.mmd
cat build/graph.mmd
flowchart LR
install["install deps · 480s"] --> compile["compile · 1080s"]
compile --> test["test suite · 720s"]
compile --> upload["upload artifacts · 120s"]
npx -y @mermaid-js/mermaid-cli -i build/graph.mmd -o static/img/graph.svg
# → 一个可以在 PR 中审查的 SVG,而不是必须盲目信任的图片
图表:只使用文章中已经存在的数字
原理相同,只是多一道防护。路由器已经从草稿中复制出了数值;模型负责编写绘图代码;代码在沙箱中运行;在图表被允许出现在页面附近之前,组装器会把渲染出的数值与原始数字重新比对。
import re, matplotlib
matplotlib.use("Agg") # 无头模式:CI 中没有显示器
import matplotlib.pyplot as plt
def assert_in_draft(value, draft: str) -> None:
"""绘制的数字必须以数字形式出现在文章中。"""
# 这里的陷阱是子串匹配:"120" 存在于 "1200" 之中,
# 也存在于 "?w=1200" 之中——朴素的 `str(v) in draft` 会对任何情况都放行。
# 要基于单词边界匹配,并接受 1 234 / 1,234 / 1234 这几种写法。
body = re.sub(r"(?<=\d)[ ,](?=\d{3}\b)", "", draft) # 去掉分隔符
if not re.search(rf"(?<![\d.]){re.escape(str(value))}(?![\d.])", body):
raise ValueError(f"{value} is not stated in the article — refusing to plot")
def render_chart(entry: dict, draft: str, out: str) -> str:
labels = [d["label"] for d in entry["data"]]
values = [d["value"] for d in entry["data"]]
for v in values:
assert_in_draft(v, draft) # 幻觉出的数值 → 不绘制图表
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=160)
ax.barh(labels, values)
ax.set_xlabel(entry["unit"])
ax.set_title(entry["title"])
fig.tight_layout()
fig.savefig(out) # 确定性、可审查的产物
return out
这道防护写起来简短,但要写得仔细:子串检查是行不通的。
对于包含 1200 或 ?w=1200 的文章来说,"120" in draft 也会成立,因此朴素写法对任何情况都会放行,等于毫无保护。要以数字边界为锚点、统一千位分隔符,这样一类会让技术文章在评论区被拆穿的错误——图表与自己所在段落自相矛盾——就无法进入生产环境。截图也遵循同样的原则:针对你真实构建版本运行的脚本化 page.screenshot(),是关于你自家界面的唯一真相来源,并且随着界面变化依然保持真实。
阶段 5:SEO 就在组装环节实现
到目前为止的一切都只是生成了文件和字段。这一阶段把它们转化为标记——这里值得写得精确一些,因为三种有文档记录的行为都是在这短短几行中决定的。
<!-- 这些字节来自另一个源站:提前完成握手 -->
<link rel="preconnect" href="https://images.unsplash.com" crossorigin>
<!-- LCP 元素:绝不懒加载,始终高优先级 -->
<figure>
<img src="{urls.regular}"
width="{width}" height="{height}" <!-- 杜绝布局偏移 -->
alt="{alt}" <!-- 在选定照片之后再撰写 -->
fetchpriority="high" decoding="async"
style="background:{color_hex}"> <!-- 主色调,1 个字段 -->
<figcaption>{attribution.html}</figcaption>
</figure>
<!-- 首屏以下的段落配图:设置相反 -->
<img src="{urls.regular}" width="{width}" height="{height}"
alt="{alt}" loading="lazy" decoding="async">
热链还是转存?上面的代码片段使用了热链,这是最快能上线的做法,也是加上 preconnect 的原因:一张来自远程的首图会在关键路径上耗费一次 DNS 查询和一次 TLS 握手,这可能会吃掉你刚用 fetchpriority 换来的收益。转存则彻底移除了第三方源站,让你能以自己的断点提供 AVIF/WebP,并且在上游 URL 变化时也不受影响——代价是存储成本、流水线中多出一个抓取步骤,以及你自己的 CDN 账单。无论你选哪种,color_hex 都能为一个字段提供占位色(模糊哈希更好看,但必须先解码成 data URI——它不是一个 CSS 颜色值)。直接把一张粗略的首图色值塞进 background:,正是大多数流水线悄悄输出一个空灰色块的地方。
-
首图绝不能懒加载。web.dev 说得很明确:“绝不要对你的 LCP 图片使用懒加载,因为那总会导致不必要的资源加载延迟,并对 LCP 产生负面影响”,并建议对很可能成为 LCP 的元素使用
fetchpriority="high"——但要谨慎使用,只用在一张图片上。4把loading="lazy"打在每一张图片(包括首图)上的流水线,是自动化发布中最常见的自伤式核心网页指标伤害。 -
始终输出
width和height——它们本就在响应中返回,没有借口不加;正是这一对属性让浏览器能够预留空间,阻止布局跳动。在文件加载期间,用blur_hash作为占位符。 -
alt 文本要在选定照片之后撰写,绝不能提前写。这是最容易被忽视的一点。计划里的
alt字段描述的是你要求的场景;而你实际得到的照片只是最接近的匹配,未必就是那个场景。把简报里的文字直接当作 alt 使用,恰恰就是这条流水线本该避免的无障碍性失误——描述一张页面上根本不存在的图片。要基于所选照片的alt_description来撰写 alt,并结合它所在的段落进行打磨。Google 的指导原则是“专注于创建有用、信息丰富的内容,恰当地使用关键词,并与页面内容保持相关”,并警告说在 alt 属性中堆砌关键词“会带来负面的用户体验,还可能导致你的网站被视为垃圾内容”。1
几乎没人做自动化的一环:许可元数据
Google 支持用于图片授权的 ImageObject 结构化数据。它要求提供 contentUrl,以及 creator、creditText、copyrightNotice 或 license 中至少一项,建议附上 acquireLicensePage,带有许可信息的图片还可获得 Google 图片中可授权徽章的资格。5上述每一个字段都已经存在于搜索响应中——所以输出它只是一个模板问题,而不是一个项目:
<script type="application/ld+json">
{
"@context": "https://schema.org/",
"@type": "ImageObject",
"contentUrl": "{urls.large}", // 必填
"creator": { "@type": "Person",
"name": "{photographer_full_name}" },
"creditText": "{photographer_full_name} on {source}",
"license": "{LICENSE_URL[source]}", // 该图库自己的许可页面
"acquireLicensePage": "{source_image_url}" // 该照片的页面
}
</script>
# LICENSE_URL 将 `source` 字段映射到真正约束
# 该照片的许可——unsplash.com/license, pexels.com/license, pixabay.com/…
值得留意的一个细节:license 应该指向真正约束该照片的许可——也就是原始图库自己的许可页面——而不是指向你自己域名下的一个摘要页面。Google 会读取这个字段来判断徽章资格,而一个自我指向的 URL 既是较弱的信号,也很难辩解成除了链回自己以外的其他东西。把你自己的许可摘要留作面向读者的内部页面,而在标记中放入规范的原始许可链接。
另外,趁你还在处理组装器时:给文件起一个简短且具描述性的名字,而不是 IMG_0042.jpg,并把图片加入站点地图——Google 的图片站点地图格式每个页面 URL 最多可接受 1,000 张图片。6这两件事在流水线里各只需要一行代码,而它们几乎从不会靠人工完成。
两套可直接照搬的流水线
同样的五个阶段,两种截然不同的形态——一种用于你自己撰写的文章,一种用于必须与正在运行的产品保持一致的文档。挑一个你能认出其失败模式的方案。
1 · CI 中的开发者博客——仓库里的 Markdown
文章以 Markdown 形式存在,图片与文章一同提交,整个流程在推送时运行。确定性强、可在 PR 中审查、运行时不依赖任何 API:
on: { pull_request: { paths: ["content/**.md"] } }
jobs:
illustrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: python plan_to_pr.py $(git diff --name-only origin/main -- 'content/*.md')
env:
PEXAFY_API_KEY: ${{ secrets.PEXAFY_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
- uses: peter-evans/create-pull-request@v6 # 图片随 PR 一起落地
with: { commit-message: "chore(content): illustrate" }
最终仍由人来批准这个 PR,这正是关键所在:流水线负责提议,审查者负责决定,而它写出的 front-matter 是可做差异对比的。
搜索部分是一次GET请求——该请求、其score_threshold以及它返回的字段,在
单篇文章指南中已逐行写明,这里直接引用而不再重印。这个文件额外增加的,是一份方案所需而单张照片所不需要的一切:对空结果简报的重试、对photo_id的占用登记(确保没有两页共用同一张图片),以及根据返回照片写出的替代文本。
import frontmatter
from photo_search import search # 一次 GET /search/photos;跳过 `used` 中已有的 id
def find_photo(entry: dict, used: set) -> dict | None:
"""搜索;如果简报过于具体,放宽一次,再不行就放弃。"""
orientation = entry.get("orientation", "landscape")
for query in (entry["query"], widen(entry["query"])):
photo = search(query, orientation, used)
if photo:
used.add(photo["photo_id"]) # 占用它:全站不重复
return photo
return None # 由调用者决定:跳过该位置,还是直接失败
def widen(query: str) -> str:
"""去掉最后一个从句——通常就是过于具体的那一部分。"""
return query.rsplit(" in ", 1)[0] if " in " in query else query
def illustrate(path: str, plan: dict, used: set) -> None:
post = frontmatter.load(path)
hero = find_photo(plan["hero"], used)
if hero is None: # 没有首图,好过配一张糟糕的首图
raise SystemExit(f"{path}: no photo above threshold — rewrite the brief")
post["hero"] = { # 模板需要的一切
"src": hero["urls"]["regular"], "w": hero["width"], "h": hero["height"],
# alt 描述的是我们实际拿到的照片,而不是我们最初要求的场景。
"alt": alt_for(hero, section=post.get("title", "")),
"bg": hero["color_hex"], "credit": hero["attribution"]["html"],
"creator": hero["photographer_full_name"], "id": hero["photo_id"],
"source": hero["source"],
"source_url": hero["source_image_url"], # → acquireLicensePage
}
open(path, "w").write(frontmatter.dumps(post))
def alt_for(photo: dict, section: str) -> str:
"""以 alt_description 为基础,裁剪到约 125 字符以适配屏幕阅读器。
若想效果更好,可结合段落再把它发回模型润色。"""
base = photo.get("alt_description") or photo.get("description", "")
return base[:125].rstrip(" ,;")
2 · 文档与更新日志——先截图,照片放在最后
把路由器的默认取向反过来。在产品文档中,最真实的视觉内容几乎总是你自己的界面:一段 Playwright 脚本,打开真实的构建版本,设定固定视口,并截取段落所描述的确切状态。示意图覆盖架构页面,照片则只出现在概念性和落地页面上——那些地方,截图什么也说明不了。
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1440, "height": 900},
device_scale_factor=2) # 视网膜屏级清晰
page.goto("http://localhost:3000/dashboard")
page.get_by_role("button", name="New API key").click()
page.screenshot(path="static/img/docs/new-api-key.png")
browser.close()
# 与文档构建运行在同一个 CI 任务中 → 截图永远不可能
# 描述一个已不复存在的界面版本。
还有两种变体值得一提,但不必单独给出完整流程。程序化 SEO把这个循环反过来了:面对由数据库生成的成百上千个页面,你不会逐页搜索——一次请求就能返回最多 100 张照片,因此你按主题集群搜索,并从一个照片池中分配,靠唯一性约束来去重
(该架构的完整介绍)。而邮件通讯和社媒卡片则需要同一张照片的四种裁切:保留 photo_id,从 urls 中请求你需要的尺寸,只有在某个裁切确实失败时才重新搜索。
同一条流水线,作为单个智能体
<<
如果已经有一个模型在写草稿,最短的路径就是直接把搜索工具交给它,而不是在多个进程之间来回传递 JSON。Pexafy 在
mcp.pexafy.com/mcp 上运行了一个托管的模型上下文协议(Model Context
Protocol)服务器。连接器的配置方式以及它所暴露的工具,其他文章已经介绍过——桌面端和编辑器端的配置见
单篇文章指南,
用于 CI 运行器的无头(headless)版本见
关于规模化内容处理的文章,
更广泛的连接器市场情况——谁在提供 MCP 图片服务器、条款如何——见
面向 AI 智能体的图片搜索基础设施研究。
这里值得展示的是,一旦智能体持有这些工具,这条流水线会发生什么变化:它不再是五个阶段,而变成了一条指令。
You Here is the draft. Build the visual plan, illustrate it, and open a PR.
Charts only from numbers already in the text.
Agent → plan: hero=photo · §2=chart · §3=diagram · §4=photo
→ search_photos(q="a developer working late at a desk with two
monitors and a mechanical keyboard in a dark room…")
← 16 photos · 147 ms · picked #1, 3000×1688, credited
→ mermaid-cli build/graph.mmd → static/img/graph.svg
→ chart.py §2 → 4 values checked against the draft ✓
→ search_photos(q="two engineers standing at a whiteboard…")
← 16 photos · 144 ms · picked #1
✓ 已填充 4 个位置 · 2 次 API 调用 · 已写入 alt + 署名 + ImageObject
⚠ §5 没有返回任何超过 0.5 分的结果——简报过于抽象,已重写为
"a person at a kitchen table checking figures on a laptop"
最后这一行正是为什么在这个循环中值得放一个智能体,而不是一段纯脚本:自动化配图的失败模式就是简报写得不好,而重写一份糟糕的简报正是语言模型的用武之地。把确定性的部分——分配、去重、数值校验——留在代码里。
一篇插图文章的成本
每篇文章三个配图位置,意味着三次搜索请求,因此免费套餐 (每月 5,000 次请求)就能覆盖 每月 1,666 篇文章的用量,还远未触及是否需要付费的问题——而如果你按主题集群而不是按文章来汇集搜索,这个上限还能再提高一个数量级。分套餐的详细拆解,以及让这个上限变得无关紧要的汇集架构,都在 这篇关于规模化内容配图的配套文章里。
每篇文章的两次模型调用——一次用于草稿,一次用于视觉计划——只消耗几千个 token,会是整条流水线里最便宜的一环;Mermaid 和 matplotlib 的渲染除了占用 CI 秒数外不花一分钱。真正让人意外的是哪一环不便宜:每篇文章生成四张图片,每月四百张图片,再加上那些没能通过筛选的尝试——而且输出的图片依然没有摄影师、没有日期,也没有来源 URL。
这条流水线解决不了什么
这是一个诚实的部分,因为它所防止的失败代价高昂。一篇配图良好的文章,依然只是一篇文章:真实的摄影、准确的图表和正确的标记,能改善一个本就值得存在的页面。它们不会让内容单薄、批量生产的内容排上名次。Google 的垃圾内容政策明确指出了规模化内容滥用——即主要为了操纵排名而生成大量页面、给用户带来极少价值,无论是否涉及自动化——而配图的质量并不是这一判断中的一个考量因素。7
因此站得住脚的定位是:这条流水线是一个你可以掌控的质量底线,应用于那些本就有理由被发布的页面。它确实能带来回报的地方在于:
- 读者可以核实的来源信息。一条带有真实摄影师和来源 URL 的署名,是一个可被核查的声明——而这些字段同样也提供给了 Google 会读取的
ImageObject标记。 - 无障碍性与核心网页指标。真实的 alt 文本、每张图片都有尺寸、首图绝不懒加载。把这些乘以你发布的每一篇文章,这就是整个网站图片质量的全部故事。
- 可核查环节的准确性。一张数值经过与文章比对校验的图表、一张从正在运行的构建版本生成的截图、一张可作为文本在 PR 中审查的示意图——这三种视觉内容,若与事实产生偏差,就会导致测试失败,绝不会悄悄溜过去。
关于署名,这条流水线特有的要点很简单:
始终展示版权信息的理由已在别处给出,而自动化组装器额外带来的是:它打印的同一条
attribution字符串,正是结构化数据所需要的creditText。一个字段,两个位置,在同一次模板渲染中输出——这正是流水线比人工更没有理由遗漏它的原因。
从哪里开始
- 把路由提示词加入到已经在撰写你草稿的任何系统中,先打印出 JSON 而不去执行它。读十份计划。如果简报里写的是主题而不是场景,先修好提示词,再动手做任何集成。
- 只接通配图位置。每份简报对应一次
GET /search/photos,并从第一天起就保存photo_id——在 3,000 个已上线页面上事后补做去重,那是一次迁移,不是加一列那么简单。 - 在同一次提交中输出 width、height 和 alt。这是你能做的、最廉价的核心网页指标优化工作。
- 然后再加入阶段 4,先做示意图再做图表——Mermaid 就是文本,所以除了语法错误之外几乎没有别的失败模式。
- 在第一张图表触达读者之前,先加上数值校验,而不是事后再补。
参考资料与脚注
1 Google 搜索中心,《图片 SEO 最佳实践》:alt 文本是“为图片提供元数据时最重要的属性”;其指导原则是打造“有用、信息丰富、恰当使用关键词、并与页面内容保持相关的内容”,避免在 alt 属性中堆砌关键词,使用 HTML
<img> 元素而不是 CSS 图片,并为文件起简短但具描述性的名字。
2 欧盟《人工智能法案》第 50 条——自 2026 年 8 月 2 日起适用的透明度义务:生成合成图像、音频、视频或文本的系统提供者,必须以机器可读格式标记输出内容,并使其可被检测为人工生成。该条款约束的是 AI 提供者和部署者;它不是关于网站可以发布哪些图片的规定。
3 Mermaid 能从受 Markdown 启发的文本定义中渲染出示意图和图表,这正是其输出内容可审查、确定性强的原因所在。
4 web.dev,《优化最大内容绘制(Largest Contentful Paint)》:“如果你认为某个 <img> 元素很可能是页面的 LCP 元素,那么给它设置 fetchpriority="high" 是个好主意”,但要谨慎使用;并且“绝不要对你的 LCP 图片使用懒加载,因为那总会导致不必要的资源加载延迟,并对 LCP 产生负面影响。”
5 Google 搜索中心,《图片元数据(结构化数据)》:ImageObject 需要提供 contentUrl,以及 creator、creditText、copyrightNotice 或
license 中至少一项;建议加上 acquireLicensePage,携带许可信息的图片可获得 Google 图片中可授权徽章的资格。
6 Google 搜索中心,《图片站点地图》:图片站点地图会告知 Google 网站上的图片,包括通过 JavaScript 发现的图片,且每个页面 URL 最多可接受 1,000 张图片。
7 Google 搜索垃圾内容政策——规模化内容滥用:主要为了操纵排名而生成大量页面、给用户带来极少价值,无论这些内容是通过自动化、人工创作还是两者结合而产生的。
资料核实于 2026 年 8 月 17 日: 图片 SEO 最佳实践 · 图片元数据 · 图片站点地图 · 优化 LCP · 搜索垃圾内容政策 · AI 法案第 50 条 · Mermaid · Pexafy API 与 MCP 文档。 搜索耗时(147 毫秒、144 毫秒)以及展示的每一张照片,均为同一天捕获的真实 API 响应。
常见问题
如何自动为大语言模型撰写的文章配图?
GET /api/v1/search/photos,耗时大约 150 毫秒)。图表和示意图类条目则交给一个负责编写绘图代码或 Mermaid 代码的模型,并以确定性方式渲染。切勿直接把文章标题送入图片搜索:标题是抽象的,没有任何照片能描绘出标题本身。文档网站应该使用截图还是素材图片?
如何防止 AI 流水线在图表中放入错误的数字?
data 字段中。然后在渲染之前用代码做断言——对每一个数值,检查其字符串是否出现在文章中,否则就抛出错误。只需九行代码,一张与自身段落相矛盾的图表就永远无法呈现给读者。自动化流水线应该为 SEO 生成哪些图片标记?
alt 文本——Google 称 alt 文本是最重要的图片元数据,并提醒不要堆砌关键词。二是每张图片都要有 width 和 height,主图上要加 fetchpriority="high",绝不能对其使用 loading="lazy",因为 LCP 图片不应被延迟加载。三是包含 contentUrl、creator、creditText 和 license 的 ImageObject 结构化数据,这正是图片获得 Google 图片“可授权”徽章资格的关键。为 AI 生成的文章配图有助于排名吗?
我该如何在CI中运行配图步骤,同时不失去编辑控制权?
photo_id进行去重、确保文章中出现的每一个绘图数值都能对应到文章正文、以及在没有任何图片达到分数阈值时直接判定失败。宁可没有主图,也不要用错误的图。