简介:这份资源是一套基于coze.cn平台与GPT技术结合的自媒体图文自动生成项目实战方案,主题围绕《历史上的今天》内容创作,面向希望实现内容自动化、批量化的自媒体运营者及AI应用开发者。方案完整覆盖了从信源筛选、爬虫插件编写、工作流搭建、AI筛选与评价、内容拓展到封面图生成和排版导出的全流程,并附有可运行的Python代码示例与提示词设计思路,便于读者直接参考和二次开发。压缩包内含1个docx文档,大小约2.18MB,核心内容为项目讲解与代码实现说明。目前已有2883人学习浏览,适合具备一定Python基础、希望借助AI工具提升图文产出效率的人群。
1. 固定模板 + 自动化流水线:把《历史上的今天》做成一条 AI 图文生产线
做小红书矩阵号的人,应该都在首页刷到过“历史上的今天”这类账号:一张固定排版的大图,配一小段历史事件介绍,每天更新,从不间断。我一开始以为这种账号背后有编辑每天手动查资料,拆完这个项目才发现,它是一条把“固定模板 + AI 流水线”用到极致的生产线——封面是加工出来的,不是 AI 现场画的;内容是从信源网站爬下来,再让 GPT 筛选、评价、拓展成文。整个流程跑完不到一分钟,产出就是一张图带一段文,直接复制就能去发布。这个项目是典型的 coze(coze.cn)+ GPT 的 AI 项目实战:爬虫插件负责取信源,大模型节点负责筛选和写文案,图像流负责盖时间戳和标题。想做自动化图文、研究 coze 工作流搭建的人,都能从这套项目里抄到不少能直接用的东西。
2. 先把信源做成插件:爬虫代码、选站逻辑与三个改动点
2.1 信源怎么选:为什么是这两个站而不是百度百科
做爬虫插件之前,先把信源选对,后面能省一个晚上的时间。我筛选信源只看四件事:内容每天更新、页面结构稳定、能直接抓、事件密度够。原项目选了国内国外两个站点,国内是 http://www.lishi365.cn/ ,国外是 https://www.onthisday.com/ ,这个选择不是随手点的,两个站都符合上面四个条件。
lishi365.cn 是典型的静态页面,每天一个“历史上的今天”列表,事件以链接形式存在,标题直接放在 a 标签的 title 属性里。这一点非常关键,因为后面那段爬虫代码提取的就是link['title'],而不是 a 标签的可见文本。很多第一次写爬虫的人会下意识去抓link.text,在这个站上就什么都拿不到,必须先想清楚目标字段藏在 HTML 的哪个位置。onthisday.com 的结构也类似,a 标签带 title 属性,适合用同一套解析逻辑。
为什么不选百度百科这类大站?动态渲染、反爬策略、页面结构频繁调整,事件还分散在词条里而不是集中在某个列表,随便一个原因是都能让本地能跑的脚本在 coze.cn 上跑不起来。做内容流水线的信源,稳定性比权威性重要,先能持续抓到数据,再考虑数据来源够不够硬。
2.2 把爬虫代码搬到 coze.cn 插件的过程
直接在 coze.cn 的插件商店里搜索《历史上的今天》能找到现成插件,但我更建议你自己建一个,因为后续换信源、换解析逻辑时,自己的插件改起来才顺手。下面是原项目里那段核心代码,可以直接贴进一个新建的 coze 插件里:
import requests from bs4 import BeautifulSoup import json import logging # 配置日志记录 logging.basicConfig(level=logging.INFO) def fetch_event_titles(url): try: # 发起 HTTP GET 请求 response = requests.get(url) response.encoding = 'utf-8' # 确保正确的编码 # 解析网页内容 soup = BeautifulSoup(response.text, 'html.parser') # 查找所有符合条件的 a 标签 event_links = soup.find_all('a', href=True, title=True) # 提取事件标题 events = [] for link in event_links: event_title = link['title'] events.append(event_title) return events except Exception as e: logging.error(f"Error fetching event titles: {e}") return [] def handler(event, context=None): try: # 目标网站 URL url = 'http://www.lishi365.cn/' # 获取事件标题列表 event_titles = fetch_event_titles(url) # 将列表转换为字符串 event_titles_str = json.dumps(event_titles, ensure_ascii=False) # 返回结果 return { 'statusCode': 200, 'body': event_titles_str } except Exception as e: logging.error(f"Error in handler: {e}") return { 'statusCode': 500, 'body': f"Error: {e}" }代码逻辑其实很直白:requests.get(url)拉取页面,response.encoding = 'utf-8'强制指定编码,然后BeautifulSoup解析 HTML,find_all('a', href=True, title=True)把同时带 href 和 title 属性的链接全部捞出来,逐个取title属性作为事件标题,最后通过json.dumps(..., ensure_ascii=False)转成 JSON 字符串返回。
有两个参数要解释清楚。第一是response.encoding = 'utf-8',中文站点不显式指定编码,BeautifulSoup 常常猜成 GBK 或者 ISO-8859-1,爬回来就是一堆乱码,这一步在中文信源上不能省。第二是ensure_ascii=False,默认情况下json.dumps会把中文转成\uXXXX形式的转义字符,返回给下游大模型节点时虽然也能用,但阅读和调试体验都很差,关掉它,body 里直接就是可读的中文。
handler(event, context=None)是 coze 插件约定的入口格式,event 是触发事件,context 可以不用管。coze.cn 的插件运行时已经预装了 requests 和 bs4,不需要在代码里做任何依赖声明。这个细节值得注意,因为如果你在本地写了爬虫再搬到 coze.cn,大概率会踩“本地能跑,平台上报错”的坑,后面避坑章节我会单独讲。
2.3 换信源时,要改的不只是 url
项目正文里有一句话很关键:“里面的 url 可以切换为你需要爬取的网站,如果爬取不成功可能是页面元素不一样,可以具体再问 GPT 解决。” 这句话其实暴露了这套代码的全部边界。
换信源时最常碰到的三种情况:第一种,页面里根本没有同时带 href 和 title 的 a 标签,那find_all返回空列表,body 就是[],这时候要把选择器改成soup.find_all('a', class_='xxx')或者直接提取可见文本link.get_text(strip=True)。第二种,目标站需要 User-Agent 伪装,否则返回 403,常见做法是给requests.get加 headers 参数,模拟浏览器访问。第三种,页面是 JS 动态渲染的,requests 拿不到任何结构化内容,这类站直接放弃,不值得为它上无头浏览器。
我一般会先在本地用同样的代码把目标信源跑一遍,确认能拿到结构化数据,再搬进 coze.cn 插件。coze.cn 的插件没有本地 IDE 那种断点调试能力,唯一的排查手段就是看日志和返回值,所以插件代码里务必保留 logging。这一段流水线上,爬虫是第一个环节,它一旦悄悄返回空列表,后面所有节点都在空转,而且不报错,属于整套工作流里最容易“黑匣子化”的位置。
3. 大模型节点不只做摘要:筛选、评价、拓展一次完成
3.1 为什么抓回来的内容不能直接用
把插件接进工作流,测试一下节点,你会看到 body 里是一长串 JSON 数组,里面可能装了几十条甚至上百条标题。直接拿这些标题去做封面,肯定不行,原因有两个:一是数量太多,一张封面只能承载一条事件,上百条里挑哪条?二是质量不齐,爬虫不区分“1993 年某地修了一条路”和“1951 年世界首次播出彩色电视节目”之间的传播价值差异。
所以工作流里需要在插件后面接一个大模型节点,干的是“编辑”的活:从一堆原始事件里挑出 3 到 5 条有画面感、有情绪价值、适合小红书的,再给每条配上评价文字,把“事件”变成“素材”。这一步是大模型在整套流水线里的真实定位,它不是在创作,是在做信息降噪和内容预排版。
3.2 提示词一定要带上{input},这一步决定后面会不会跑飞
原项目里有一句提醒:“提示词里一定要附上变量名,比如这里的{input},这样模型才知道你要加工的内容是什么。” 这算是我见过最容易被忽略的 coze 工作流细节。
coze.cn 大模型节点的输入变量,是通过占位符{input}注入到提示词里的。你在提示词里不写{input},模型就拿不到上游插件抓回来的内容,它只会按自己的知识库自由发挥,输出一堆跟当天历史事件毫无关系的东西。我第一次搭的时候就吃过这个亏,节点配置里明明已经连了上游,提示词却忘了写{input},模型直接开始给我写“历史事件的重要性”这种空话,整个筛选节点等于白跑。
下面这个提示词模板可以直接抄,核心是把输出格式锁死:
请对下面{input}中的历史事件列表进行分析。 1. 筛选出 3~5 条最有传播价值的事件,标准是:画面感强、有情绪共鸣、适合小红书图文展示。 2. 对每条事件,按以下固定格式输出。 时间:{具体日期} 标题:{事件标题} 评价:{100~200字的小红书风格评述,口语化,有观点} 不要输出其他内容,不要加序号以外的格式。这段提示词的要点有二。第一,{input}必须原样写在提示词里,它是 coze 的变量插槽,不是写给你自己看的注释。第二,输出格式里“时间”“标题”“评价”这三个字段名一个都不能改,因为下游还有一个代码节点用正则按这三个关键词切字符串,字段名对不上,代码节点提取出来的就是空值。这条流水线每一环的输出格式,都被下一环的解析方式锁死了。
3.3 模型参数怎么设:温度和输出格式
大模型节点不是拉到默认参数就能跑的,有几个参数需要手工调。
| 参数 | 建议取值 | 说明 |
|---|---|---|
| 模型 | coze.cn 可用的 GPT 系列或等价大模型 | 筛选任务不需要最强模型,性价比优先 |
| 温度 temperature | 0.3 ~ 0.7 | 太低则每次筛选结果雷同,太高则格式容易飘 |
| 输出长度 | 1000 ~ 1500 字 | 3 到 5 条事件的评价,留足空间 |
| 输入变量 | {input} | 没有变量注入,整个节点就是空转 |
温度这个参数,是玄学也是实际经验。我一般给 0.5,既能保证筛选结果稳定,又不至于连续几天选出同一批事件。如果你的目标是每天内容不重复,可以把温度拉到 0.7,代价是偶尔会出现输出格式不规整,后面正则提取时要做好容错。
筛选和拓展放在同一个节点里完成,是我建议的做法。原项目里是先筛选再拓展,实际操作中可以合并,让模型一次输出“时间、标题、评价”三段结构,既能省一个节点,也避免了两段输出之间字段名不一致的问题。节点越少,工作流调试越省心。
3.4 选择器范围过宽导致的噪声
爬虫用的find_all('a', href=True, title=True)是全站范围扫描,不限定某个区块,所以抓回来的内容里除了“历史上的今天”事件,可能还混着导航链接、推荐位、站内其他页面的标题。大模型节点虽然能做筛选,但如果噪声占比太高,模型也有可能把导航链接当成事件来处理。
处理方式有两个:一是在爬虫代码里先缩小范围,soup.find('div', class_='list').find_all('a', ...),把解析范围限定到事件列表容器;二是靠大模型节点提示词里的“只保留历史事件,忽略非事件内容”来二次过滤。我在实际跑的时候两种都会做——爬虫先粗过滤,大模型再精筛,双保险。这一步不做,你会发现封面上的标题偶尔会冒出一条“关于本站”之类的奇怪内容,极其尴尬。
4. 代码模块拆变量:正则、传参类型与下游变量对齐
4.1 为什么要拆变量
大模型节点输出的是一段完整文字,里面有“时间”“标题”“评价”三部分。但下游的用途是分开的:图像流只需要日期和标题,用来压到封面图上;小红书的正文只需要评价部分。一个节点输出多个字段,在 coze.cn 工作流里不能直接把这个长文本整体传给图像流,否则图片上会压上一大坨文字。
所以需要插入一个代码模块,把一段文字拆成三个独立变量。这个节点在整条流水线里的角色就是“拆件”,不复杂,但缺了它前后就接不上。
4.2 正则提取代码(精简版)
原项目附的代码里有大量 print 调试语句,是为了在平台上排查传参类型用的,真正在线运行不需要那些。我把它精简成下面这个版本,直接复制到 coze.cn 代码模块里就能用:
import re def main(*args): # coze 的代码模块会把上游节点输出作为参数传进来 if not args: return {"error": "no input"} raw = args[0] # 兼容三种常见传参:字符串、带 content 属性的对象、dict if isinstance(raw, str): text = raw elif isinstance(raw, dict) and "params" in raw and "input" in raw["params"]: text = raw["params"]["input"] elif hasattr(raw, "content"): content = raw.content if isinstance(content, str): text = content elif isinstance(content, dict) and "params" in content: text = content["params"].get("input", "") else: return {"error": f"unsupported content type: {type(content)}"} else: return {"error": f"unsupported arg type: {type(raw)}"} # 正则提取日期、标题、评价 date_match = re.search(r"时间:(.*?)[\n,]", text) title_match = re.search(r"标题:(.*?)[\n,]", text) content_match = re.search(r"评价:(.*)", text, re.DOTALL) return { "day": date_match.group(1).strip() if date_match else "", "title": title_match.group(1).strip() if title_match else "", "content": content_match.group(1).strip() if content_match else "" }这段代码有三个关键点。第一,re.search(r"时间:(.*?)[\n,]", text)用了非贪婪匹配,意思是匹配“时间:”后面到第一个换行符或逗号之前的内容,防止把整段评价文本也吞进去。第二,re.search(r"评价:(.*)", text, re.DOTALL)里的re.DOTALL让.可以匹配换行符,因为评价部分往往有 100 到 200 字,中间带换行,不加这个参数,正则只会截到第一行。第三,所有匹配结果都做了.strip(),去掉首尾空格和换行。
我用正则而不是直接split切字符串,是因为正则靠关键词定位,就算大模型输出时字段顺序有点乱,只要“时间:”“标题:”“评价:”这三个标记还在,就能正确提取。split方式只适合严格固定格式,在大模型输出面前太脆。
4.3 coze 代码模块的传参坑:它给你的可能不是字符串
coze.cn 的代码模块在传参上像个黑匣子。你以为上游大模型节点输出的是字符串,实际传进来的可能是一个带content属性的对象,甚至是一个嵌套的 dict。这就是为什么上面代码里做了三层类型兼容:字符串直接转文本;dict 从params.input里取值;对象从content属性取值,取出来之后还要再判断一次结构。
原项目代码里那一大段print(f"Arguments received: {args}")和for attribute in dir(arg):循环,就是在排查这个东西。遇到传参异常时,别急着改正则,先把代码模块的返回值设成{"raw": str(args), "type": type(args).__name__},把实际结构打出来看一眼,你就知道该从哪个字段取值了。这一步能救命的,因为 coze 改版改过好几次传参结构,网上能搜到的老教程大概率对不上当前版本。
4.4 输出变量的命名要跟下一个节点对齐
代码模块的输出是一个 dict,key 是day、title、content。这些 key 在 coze 工作流里会变成下游节点的可选变量。图像流那边的时间加工节点要选day,事件加工节点要选title,小红书文案节点要选content。
| 代码模块输出 | 下游节点 | 用途 |
|---|---|---|
| day | 图像流“时间加工节点” | 封面上方的日期文字 |
| title | 图像流“事件加工节点” | 封面下方的事件标题 |
| content | 后续大模型节点 | 加工成小红书正文 |
变量名对不上,最典型的现象是下游节点的变量下拉列表里看不到day、title、content这三项,或者看到了但选错了,导致图片上盖了错误的日期。每次搭工作流,我强烈建议先让代码模块跑一次测试,确认返回值里三个 key 都有值,再往后接节点。组件少的时候不好排查,但一旦接错了,封面图会一直出问题,返工成本比多花两分钟检查高得多。
5. 避坑与排查:跑通这套工作流之前,我先踩过的七个坑
5.1 插件与爬虫相关的坑
坑一:插件在 coze.cn 上测试时返回 500,本地却跑得好好的。
现象:插件节点测试时 statusCode 是 500,body 里显示 Error,但同一段代码在本地 Python 环境里运行完全正常。
原因:coze.cn 的插件运行环境是受限的在线沙箱,它的网络出口和本地不在同一个网络环境,部分网站的访问策略会直接拒绝来自数据中心的请求,返回 403 或者直接超时。另外,本地见过的浏览器 UA 头,在沙箱里可能变成默认 Python 请求头,也会被目标站拦。
解决:在代码里加上浏览器的 User-Agent,伪装成一个正常的 Chrome 请求,再把超时时间放开一点。如果加了 UA 还是 500,换一个信源站,不要跟反爬硬刚。爬虫能抓到数据是第一优先级,用哪个站抓反而不重要。
坑二:返回 200,但 body 里是空数组[]。
现象:插件节点测试看似正常,不报错,但 body 里是[],一个标题都没有。
原因:目标站点的页面结构跟你预期的不同。代码里find_all('a', href=True, title=True)是严格限定“同时有 href 和 title 属性”的 a 标签,如果目标站的事件列表用的是class定位,或者标题存在li标签里,这段逻辑就什么都捞不到。
解决:把爬虫代码先拿到本地跑一遍,用soup.prettify()打印页面结构,确认目标字段在哪个标签里,再改代码。在 coze.cn 上你怎么改都要反复提交测试,效率太低。
5.2 大模型节点与代码模块相关的坑
坑三:代码模块提取到的 day、title、content 全是空字符串。
现象:代码模块测试成功,返回值里三个 key 都在,但 value 全是""。
原因:大模型节点的输出格式跟预期不符。可能是“时间:”写成了“日期:”,或者输出里多了 Markdown 加粗符号**时间:**,正则匹配不上。
解决:在提示词里把输出格式锁死,明确要求不要加 Markdown 符号、不要加额外说明,字段名严格用“时间:”“标题:”“评价:”。代码模块的正则也顺手兼容一下\*\*时间:\*\*这种带 Markdown 符号的情况,做法是正则里加[\*\s]*容错。
坑四:同一个工作流,前后跑两次输出结果完全不同。
现象:上午跑出来的评价是一条,下午跑出来变成另一条,有时候筛选出的事件都不同。
原因:大模型节点的 temperature 参数太高,模型每次采样偏好都不同,筛选结果漂移。
解决:把 temperature 调到 0.5 以下。内容型任务需要一定的随机性来避免每天重复,但筛选任务需要稳定,我一般会拆成两个节点——一个低温做筛选,一个中温做评价拓展,两个节点的提示词和温度分开配置。项目正文里把筛选和拓展放在同一个节点,是省节点数的做法,对稳定性有要求的场景建议拆开。
坑五:代码模块报错 text is undefined。
现象:能把上游内容打印出来,但代码一直报变量未定义。
原因:coze.cn 代码模块里,如果用了main(*args),上游传参不是按位置参数进来的,或者没有触发main的调用入口,导致代码逻辑里某个分支的变量没有赋值。原项目代码末尾有一段class Args和main(user_input)的模拟调用,这段在 coze 在线环境里其实是多余的,coze 会自己调用函数入口,你手动调一次反而容易引入脏数据。
解决:直接删掉末尾的模拟调用代码,只保留main函数定义。如果不确定平台怎么唤起入口,就用 coze 代码模块自带的模板结构,别自己造调用方式。
5.3 图像流与封面的坑
坑六:图片上中文变成方框或者乱码。
现象:封面图生成后,时间位置出现一排方框,标题位置全是乱码,英文字母正常,中文全废。
原因:图像流加工节点里用的字体不支持中文。coze.cn 的图片处理默认字体往往是英文字体,中文字形缺失。
解决:在图像流的文字参数里显式指定中文字体,或者上传一张带有中文字体的底图,把文字压上去而不是依赖节点自动渲染。我自己的做法是背景图里预留好文字区域,日期和标题用稳定支持中文的字体渲染,提前测一张样图再批量跑。
坑七:封面图上的事件标题跟日期对不上。
现象:日期显示的是今天的日期,标题却是昨天的事件,导出的图片内容互相错位。
原因:工作流变量绑定错了。图像流里时间加工节点绑定的是day变量,事件加工节点绑定的是title变量,两个节点如果被重复使用了同一个变量名,或者下游节点从旧版本工作流继承了一个固定的测试参数,就会出现这个情况。
解决:把图像流里的每个文字参数都打开确认数据来源,必须是上游代码模块输出的变量,不能是写死的测试值。这个坑容易出现在你复制了一个旧工作流然后改新信源的情况下,旧节点会自动带着上次跑的测试值,改的时候只改了信源没改图片流变量,就会错位。
6. 从一套工作流到一类账号:换信源和封面就是新模板
6.1 三个变量,决定一个账号的方向
这套工作流跑通之后,你会发现自己手里其实是一个“模板”,不是一个“项目”。它能复用的原因在于,整条流水线里真正变化的东西只有三样:信源、封面背景、提示词方向。一次搭建,后面每次换赛道,成本不到十分钟。
原项目正文里给了几个方向:刚刚发生了什么、信息差、当地新闻、副业信息。我拆一个“副业信息”的例子:信源换成某个副业资讯站,封面背景换成颜色更犀利的底图,大模型节点的提示词从“筛选历史事件”改成“筛选有落地可能性的副业案例,评价里写明操作门槛和启动成本”。输出结构不变,还是“时间 / 标题 / 评价”三段,下游代码模块和图像流完全不用动。
这个思路最值钱的地方在于:它把内容生产的稳定性交给了模板,把创意判断交给了大模型,把重复劳动交给了工作流。每天固定更新一个账号,需要的不是灵感,是一套能稳定运行的结构。
6.2 上线前强制走一遍的验证清单
换完模板,别急着上线,我会强制自己跑一遍下面的验证清单,整套流程不到五分钟,但能拦住绝大多数翻车:
| 检查项 | 怎么验证 | 容易出现的问题 |
|---|---|---|
| 信源连通性 | 插件节点单独测试,看 body 里有没有数据 | 反爬拦截、页面结构变化、返回空数组 |
| 筛选质量 | 大模型节点单独测试,看筛选出的事件是否贴合主题 | 提示词没带 {input}、温度太高导致跑偏 |
| 代码模块提取 | 看 day / title / content 三个字段是否有值 | 正则匹配不到、上游字段名不一致 |
| 封面文字 | 图像流输出一张样图,肉眼核对日期和标题 | 中文乱码、文字位置溢出、变量绑定错位 |
| 正文长度 | 检查 content 字数是否在小红书允许范围内 | 评价文字过长被截断、需要手动删减 |
从那以后,我每次搭 coze 工作流都会强制自己先跑一遍全链路测试,把每个节点的返回值 dump 出来看一眼再继续。这不是玄学,是流水线上游错一个字,下游整张图都会出问题。提前五分钟验证,好过上线后被读者评论指出错误再回来改。希望帮到你。
本文还有配套的精品资源,点击获取