让 AI Agent 自己发文章:发布接口的流程设计

让 AI Agent 自己发文章:发布接口的流程设计

一、起点:发文流程里最慢的一环是人

站点上的文章,"写"和"发"其实是两件事。写是创作,发是搬运——登录后台、新建文章、粘贴正文、选栏目、填摘要、传图、点发布。偶尔发一篇,这套流程完全没问题;但当文章本身就由 AI 生成、发文变成常态时,搬运就成了整条链路上最慢、也最容易出错的一环。

粘错栏目、漏填摘要、图片传了却忘记插进正文——这些都不是假设,是真实发生过的。于是有了这套接口:让 Agent 直接通过 HTTP 把文章写进 CMS,去掉中间的人工环节。

本文不是接口手册,讲的是这套流程为什么这么设计。因为面向机器的接口和面向人的接口,设计要求并不一样:人要的是提示清楚,机器要的是结果可判断、错误可自解释、操作不可逆就要能防重

二、整体流程:四步,顺序即契约

整套流程只有四个接口,但顺序不能乱:

四步工作流:先查栏目、再查字段、上传图片、提交文章

这个顺序本身就是设计的一部分:

  • 先问有哪些栏目——栏目 id 不硬编码,站点新增栏目时调用方无需改代码;
  • 再问该栏目要填什么字段——不同栏目字段可能不同,一切以接口返回为准;
  • 然后才上传图片——正文与头图只能引用站内地址,得先拿到 URL 才能写 HTML;
  • 最后提交文章——此时所有素材都已就位,提交是一次性的。

顺序对了,调用方就永远不需要"预知"站点结构。这一点对 AI Agent 尤其重要:Agent 不该靠记忆里的字段表工作,而该靠接口当下的回答工作。

三、第一步:让调用方不迷路——字段自描述

最容易被忽略、却最能决定成败的,是第二步那个字段查询接口。

GET /api/cms/fields?cate_id=17

{
  "code": 1,
  "data": {
    "cate":  {"id": 17, "cate_name": "AI学习", "cate_folder": "Ai"},
    "fields": [
      {"field": "title",  "name": "标题", "type": "text",   "required": true},
      {"field": "status", "name": "状态", "type": "radio",  "required": true},
      {"field": "image",  "name": "图片", "type": "image",  "required": false}
    ],
    "upload": {"url": "/api/cms/upload", "field_name": "file",
               "allow_ext": ["jpg", "png", "gif", "jpeg", "ico"]}
  }
}

它一次性回答了三个问题:这个栏目叫什么要填哪些字段、哪些必填图片能传什么格式、往哪传

为什么不能把字段表写死在调用方?因为那样一来,后台加一个字段,所有调用方都得跟着改;而后台改字段是运营日常,调用方发版不是。把"我该填什么"变成一次运行时查询,接口就永远不会和调用方脱节。

设计经验:凡是会变的东西,都不要写进调用方。让它每次开口问。

四、第三步:图片必须先落到站点

图片走独立的上传接口,单张字段名用 file,多张用 file[]

POST /api/cms/upload
Content-Type: multipart/form-data

file=@cover.png

→ {"code": 1, "data": {
     "url": "/uploads/20260922/bb2f3583....png",
     "urls": ["/uploads/20260922/bb2f3583....png"]
   }}

返回的是相对路径,可以直接原样写进 image 字段和正文的 <img src>,前台会自动拼接域名——调用方不需要关心站点部署在哪个域名下。

这里有两个容易踩的点:

  • 不能直接引用外站图片。把别处的图链写进正文,可能不显示或被过滤。正确做法是先下载到本地,再上传到站点,然后引用站内地址。
  • 服务端会校验真实图片内容。改扩展名蒙混过关会被拦下,返回"文件不是有效的图片"。

五、第四步:正文是 HTML 片段,不是 Markdown

提交接口接收 JSON,正文以 HTML 片段原样渲染。这意味着两件事:

其一,别提交完整 HTML 文档。不要 <!DOCTYPE><html><head><body>,只给正文那一段。页面骨架由站点模板负责。

其二,别内联样式。不要给标签加 style 属性,也不要用 <div> 做布局——这些会与站点样式打架,轻则难看,重则破版。想让排版好看,靠语义标签

标签用途
<h2>一级小节标题(正文内不用 <h1>,页面标题已占位)
<h3>二级小节标题
<p> / <ul> / <ol>段落与列表
<blockquote>引用、金句
<pre><code>多行代码块
<table>简单表格

还有一点:写入前要自行转义正文里的 <>&。文章讲代码时最容易撞上——正文里要展示一个 HTML 标签,直接写就会被浏览器当成标签吃掉。

六、四条设计红线

四条设计红线:自描述、可判断、不可逆、失败隔离

1. 自描述:接口自己说清楚要什么

前面已展开。核心是:任何会变的信息都不该固化在调用方。栏目会变、字段会变、允许的图片格式会变——让它们都从接口来。

2. 可判断:HTTP 200 也可能是业务失败

几乎所有接口都返回 HTTP 200,业务成败体现在响应体的 code 字段:

{"code": 1, "msg": "OK",      "time": 1789718925, "data": {...}}
{"code": 0, "msg": "鉴权失败:X-Api-Token 无效", "data": []}

只看 HTTP 状态码就判断成功,是这类接口最经典的误用。 code=1 才算成功。

更进一步,失败时的 msg能自解释——不是抛一个"操作失败",而是明确告诉调用方错在哪:

  • 鉴权失败:X-Api-Token 无效 —— 令牌问题,检查请求头;
  • 栏目不存在:cate_id=99 —— 栏目 id 错了,回到第一步重查;
  • 该栏目不支持发布文章(单页模型或非内容模型) —— 这个栏目不是文章栏目;
  • 文件不是有效的图片:a.txt —— 文件内容不对,不是改扩展名能解决的。

错误信息写得越具体,Agent 越能自己纠正。面向机器的接口,报错不是给运维看的日志,而是给调用方的操作指引。

3. 不可逆:只增不改不删,就该防重

这套接口只支持新增,不提供修改与删除。这是刻意为之:

  • 文章一旦发布,URL 立即生效,可能已被搜索引擎收录、被读者转发。允许随意改删,风险大于便利;
  • 更重要的是——调用方是 Agent。对不可逆操作,Agent 必须被明确告知"不要重试"。
重复提交相同标题,不会覆盖,而是新增一条。所以:POST 一旦返回成功,绝不因网络抖动而重试。

这条约束必须写进 Agent 的规则里,而不是指望它自己悟出来。

4. 失败隔离:一个环节挂了,不该拖垮全部

发布时,系统会顺带在微信公众号草稿箱建一份草稿——正文排版、图片转存、封面、阅读原文链接都会自动处理好。但这一步失败不影响站内发文

{
  "code": 1,
  "data": {
    "id": 122,
    "url": "/index/Ai/122.html",
    "wechat": {"draft": true, "media_id": "uXgc...", "error": ""}
  }
}

即使 draftfalsecode 仍然是 1——站内文章已经发布成功,公众号没建上只体现在 wechat.error 里。

这是多端分发时的重要原则:把"次要环节的失败"和"主要目标的成败"分开表达。如果两者混在一个状态里,一次公众号接口抖动就会让调用方以为整篇都失败了——然后重试,然后在站内多出一篇重复文章。

另外,公众号侧只能建草稿、不能自动发布(接口没有该权限),最终发布仍需人工在后台确认一次。把"最后一下"留给人,也是设计的一部分。

七、正文之外:还有一层是"内容之外"的约束

接口解决的是"怎么发",还有一层是"发什么"。

当发布权交给自动化流程后,真正需要额外守护的,是内容本身。具体的做法是:把约束明确写进调用方的规则里,而不是寄希望于模型自觉——不违法、不碰争议话题、涉及政策表述一律采用官方规范口径。这类规则写一次、长期生效,比事后删稿成本低得多。

还有一条工程上的经验:把"校验"和"发布"分成两次执行。如果把字数检查、标签规范、结构校验和最终的发布调用写进同一个脚本,那些检查就只是被打印出来,拦不住后面的提交——而接口改不了也删不了,文章只能带着瑕疵上线。凡是检查与不可逆动作同处一个脚本,检查就形同虚设。

八、小结

这套流程真正的价值,不在于省掉了多少次手动粘贴,而在于它把"发文章"从人的操作变成了可编程的动作。而要做到这一点,需要为机器重新考虑几件事:

  • 会变的东西不要固化——栏目、字段、格式,都让接口现场告诉你;
  • 成败要能被程序判断——HTTP 200 不算数,code 才作数;
  • 错误要能自解释——报错是给调用方的指引,不是给运维的日志;
  • 不可逆操作要显式防重——只增不改不删的接口,必须先教会调用方"别重试";
  • 失败要能隔离——次要环节挂了,不能让主流程误判为整体失败。

接口是给机器用的,但设计它的时候,想的仍然应该是人——想象调用方在半夜三点、没有任何上下文的情况下,会不会把这件事做对。