ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI定时任务实战:从cron到Agent自动生成日报

AI定时任务实战:从cron到Agent自动生成日报 你有没有过这种经历早上到工位先开新闻再翻邮件和群消息接着去后台查数据折腾一两个小时才搞清楚昨天到底发生了什么日报还没开始写。后来我把这件事整个交给了定时任务——不是过去那种只会“到点执行脚本”的定时任务而是会抓数据、会总结、会判断的 AI Agent。这个组合最近在各个技术社区讨论度特别高。本质上是把原先定时任务的“机械触发能力”和大模型的“理解生成能力”拼到一起让脚本不仅能定时跑还能定时“动脑子”。写日报、看新闻、做计划、盯价格这些听起来需要人工完成的事确实已经可以全自动跑起来了。这篇文章我不讲虚的从设计思路、cron 语法、调度框架选型到具体怎么一步步搭一个能用的 AI 定时任务系统再附上我实际跑下来踩过的坑一次性讲透。1. 先搞懂 AI 定时任务的本质把“死执行”变成“会思考”1.1 传统定时任务和 AI 定时任务的本质区别传统定时任务大家很熟了Linux 下的 crontab、Java 里的 Quartz、Spring Boot 的 Scheduled、运维常用的 xxl-job、Kettle 里的定时同步本质上都是同一条逻辑到时间了执行一段写死的代码或脚本输出永远是可预期的。比如每天凌晨拉取一次数据库同步每天上午给用户发一条固定推送。它擅长解决的是“重复的、确定性的活”。但这类任务有个天花板它只能执行不能生成。你让它每天抓 100 条行业新闻它只能把链接和标题列出来没法告诉你这 100 条里哪些和你的业务强相关更没法把前因后果梳理成一段能直接贴到日报里的文字。想让机器做这种活原来的方案是人在流程里补位——人来看、人来总结、人来写。这也是为什么很多公司自动化做了好几年日报、周报、竞品分析这类工作仍然天天耗人。AI 定时任务把这一层补上了。它保留了定时触发框架但把原来“脚本直接输出结果”改成“脚本先采集数据再交给大模型生成结论”。同样是早上八点跑一个任务采集到的不再是十个孤立的数据点而是一段已经归纳好的摘要、三条业务建议、一张可以拿去发群的价格异动清单。定时任务仍然负责“准时”AI 负责“思考”各干各的活。1.2 三段式架构定时器、数据源、大脑我做了几个 AI 定时任务项目之后逐渐习惯把整套系统拆成三个独立部分任何一部分坏掉都不会拖垮另外两个定时触发器负责在指定时间点启动任务常见的有系统 crontab、APScheduler、Spring Boot 调度器。数据采集层负责从目标源抓取原材料比如资讯接口、商品价格接口、内部系统报表、数据库。生成与推送层把采集结果整理成 Prompt调用大模型生成日报、摘要、计划再通过钉钉、企业微信、邮件等方式推到你手机或电脑上。为什么要拆这么细因为三个部分的“故障特性”完全不一样。定时器是最稳定的只要服务器不宕机基本不出错。数据采集层最容易出问题目标源的页面改版了、接口限流了、登录态过期了都有可能让这一层挂掉。大模型调用也不完全可靠偶尔超时、偶尔返回格式不对。如果把三层写死在一个脚本里数据源一抖动整个任务就废了拆开之后数据采集失败只是这一轮没有新材料推送逻辑还可以发一条“今日数据获取失败”的告警不至于毫无感知。这个结构还带来一个额外好处每一层都能独立升级。今天想换一个更强的模型只改生成层明天想多加一个数据源不需要动定时器后天想把日报从早上八点挪到早上七点半只改调度参数。对于个人项目来说这种“可替换性”比什么都重要。1.3 为什么现在才开始“火”说实话AI 能力不是今天才有的定时任务更不是什么新东西。真正让这两个词凑到一起爆发的原因是三个条件同时成熟了。第一大模型 API 的价格降到了个人能随便跑的程度很多时候一次调用成本只有几分钱几十个任务的月成本甚至不如一顿外卖。第二各家模型对输出格式的控制能力明显增强能用 JSON 约束输出内容这让程序可以稳定解析生成结果而不是天天跟大模型“猜谜语”。第三AI Agent 的概念普及让更多人意识到模型不只能聊天还能作为一个“函数”被嵌进自动化流水线里充当中间层做判断、提炼、归纳。这几件事叠加在一起让原本“写脚本的人不会调模型、做算法的人不熟运维”的断层消失了大半。现在一个懂点 Python 的普通开发者就能搭出过去需要一整个团队才能维护的自动化工作流。2. 定时任务核心语法与调度框架选型2.1 cron 表达式5 分钟看懂这个“定时咒语”在动手搭项目之前定时触发这关必须先过。不管是 Linux 的 crontab还是 Java、Python 的定时框架绝大多数场景都绕不开 cron 表达式。很多人一看到这串字符就头疼其实它的规则特别简单就六个字段代表“分 时 日 月 周”。标准格式是这样分 时 日 月 周分0-59时0-23日1-31月1-12周0-70 和 7 都表示周日或 MON-SUN每个字段里可以出现星号、逗号、减号、斜杠。星号表示“每”逗号用于列举多个值减号表示区间斜杠表示步长。我最初接触的时候是把这段规则抄在便利贴上的后来发现记住三个具体例子就能覆盖九成场景。# 每天早上 8 点 30 分执行 30 8 * * * # 每 10 分钟执行一次 */10 * * * * # 工作日周一至周五早上 9 点整执行 0 9 * * 1-5定时内容越复杂越要小心“日”和“周”两个字段之间的关系。很多框架里如果日和周同时配置了限制判定逻辑是“或”而不是“与”也就是两个条件满足其一就会触发带来的直接后果就是你以为“每月 1 号且只在周一执行”结果每个月 1 号无论是不是周一都跑了。这个问题我在踩坑环节会再展开。2.2 主流调度框架怎么选定时触发层看起来只是“到点干活”实际选型要结合你的技术栈和使用场景。我整理了一张关于常见方案的对比方便你快速判断方案适合场景优势注意点Linux crontab服务器上的轻量任务系统自带简单稳定只有分钟级精度没有失败重试日志得自己管Python APScheduler个人脚本、快速原型、中小项目代码内配置调度支持 cron 和 interval 两种方式支持持久化多进程部署时需要加锁防止重复执行Java ScheduledSpring Boot 项目原生集成上手快默认单机执行分布式环境下会有重复触发问题xxl-job中大型分布式系统自带调度中心、失败重试、日志追踪、集群执行需要额外部署调度中心运维成本较高Kettle / SpoonETL 数据同步图形化配置适合数据管道定时抽取不适合做 AI 生成类任务对 API 调用支持较弱个人做 AI 定时任务两个比较建议的方向是如果脚本主要跑在 Linux 服务器上直接用系统 crontab 最省事如果你想在代码里管理调度逻辑、动态添加任务、并且有失败重试的需求Python 的 APScheduler 会是更顺手的选择。如果你的项目本身是 Spring Cloud 架构还要解决多个服务实例下的任务互斥那就应该在 xxl-job 这类分布式调度平台上做让调度中心统一分配执行器而不是在业务代码里各自写定时器。2.3 我给出的选型参考我自己现在跑的几个 AI 定时任务用的是“APScheduler 独立任务脚本”的组合放在一台低配云服务器上。这套组合的好处是迭代速度快今天想加一个数据源直接改 Python 文件重启一下调度器就好。任务少、逻辑简单不需要上个调度中心。管理多个任务时只需要提前把调度规则集中写在一个配置文件里每次改动不用去服务器上翻 crontab。如果你的定时任务要跑在 Java 系应用内部比如已经有一个 Spring Boot 服务每天需要定时汇总业务数据并生成运营日报那我建议直接用 Scheduled 起步把任务逻辑封装成独立的 Service 方法调度表达式写在配置中心统一管理。等业务量大到需要多实例部署时再把任务迁到 xxl-job 上把执行逻辑抽成执行器就行不要一开始就套重型框架。3. 实战从零搭建一个 AI 日报自动生成与价格盯梢任务3.1 目标拆解我拿一个实际跑了好几个月的项目来说目标是每天早上 8 点产出三样东西一份当日要闻摘要、一份岗位相关的工作日报草稿、一份重点商品的价格异动提醒清单。拆成代码结构就是四个子任务抓取从公开资讯接口、内部业务接口、价格监控接口拉取数据清洗把上一步拿到的原始 JSON 整理成结构化的文本生成调用大模型 API把文本丢给模型生成摘要和日报草稿推送把结果发送到企业微信群里同时存一份 Markdown 到本地这套流程跑通了以后你要盯任何新东西都只需要在“抓取”和“清洗”两个环节里加一个数据源后面全部复用。3.2 数据采集层让任务“有料可吃”采集层的设计要点是“能接 API 就不要写爬虫”。API 的字段相对稳定不容易因为页面改版而挂掉。如果要采的数据源只有网页那就退而求其次写爬虫但一定要做好异常捕获和频率控制别把自己的服务器 IP 搞进对方的封禁名单。下面是一段简化后的示例演示怎么用 Python 抓取资讯列表和商品价格import requests import json HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) } def fetch_news(): 从公开资讯接口获取热门新闻列表这里用示例接口示意 url https://your-news-api.example.com/api/hot resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() items resp.json().get(data, []) cleaned [] for item in items[:15]: cleaned.append({ title: item.get(title), link: item.get(url), source: item.get(source), }) return cleaned def fetch_price(sku): 抓取指定商品的最新价格sku 为商品编码 url fhttps://your-price-api.example.com/sku/{sku}/price resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() data resp.json() return { sku: sku, price: data.get(price), ts: data.get(update_time), }有几个细节容易被忽略。第一所有 HTTP 请求都要设置 timeout最好在 10 秒以内防止某个接口长时间不返回把整个任务卡死。第二使用raise_for_status()接口一返回 4xx 或 5xx立刻抛出异常并终止本次采集不要拿到一个错误页面还在后面硬解析。第三单次任务采集量不需要追求多比如通讯类内容抓 10-15 条就够大模型做摘要了抓 500 条反而会稀释重点、增加 token 成本。凡是涉及人工登录态的接口更建议单独抽成函数并通过配置文件传入 token 或 cookie不要写死在代码里。我之前有次图省事把 token 写死在脚本里过期后排查了大半天才发现问题。现在统一从环境变量读取脚本写得更干净还减少了误提交密钥的风险。3.3 用大模型生成日报Prompt 是关键材料到手后下一步是调用大模型。这里不是把原始数据一股脑丢给模型就完事Prompt 的设计直接决定了输出质量。我的经验是给大模型一个“角色 输入材料 输出格式要求”的结构要比简单说一句“帮我总结新闻”稳定得多。下面这段代码展示了我常用的调用方式使用标准的 OpenAI 兼容接口from openai import OpenAI client OpenAI( api_keyyour-api-key, base_urlyour-api-base-url ) def generate_report(news_items, market_items): # 先把采集到的数据序列化成文本作为上下文材料 content 今日要闻\n for idx, item in enumerate(news_items, 1): content f{idx}. {item[title]} ({item[source]})\n content \n商品价格变动\n for item in market_items: content f- SKU {item[sku]} 当前价格 {item[price]}\n prompt f 你是一名业务顾问请根据以下原始信息生成一份今日简报。 要求 1. 用自然语言写出 3-5 条关键要闻摘要每条不超过 50 字 2. 单独列出值得继续跟进的方向最多 3 条 3. 价格部分只标记环比昨日有明显变动的商品 4. 最后用 JSON 格式输出{{summary: , focus: [], price_alerts: []}} 5. 所有结论必须基于输入材料禁止编造。 输入材料如下 {content} resp client.chat.completions.create( modelyour-model-name, messages[ {role: system, content: 你是一个严谨的数据分析助手输出稳定不编造信息。}, {role: user, content: prompt}, ], temperature0.3, ) return resp.choices[0].message.content注意这里 temperature 设成了 0.3而不是默认或更高的值。因为在定时任务场景里我们希望输出尽量稳定、少发散。temperature 越低生成内容越保守、越贴近输入材料温度太高同一个输入可能每次生成完全不同的摘要后续解析 JSON 的难度也会上升。模型返回的内容格式不一定完美解析 JSON 时一定要做异常兜底。我习惯先尝试用json.loads()直接解析如果失败就用正则把代码块里的 JSON 片段挑出来再解析一次实在解析不了就原样存日志等待下一轮重试而不是让任务直接崩溃。3.4 调度与主流程把任务串起来数据采集和生成逻辑都准备好后把它们挂到 APScheduler 上。调度器使用BlockingScheduler设置时区为Asia/Shanghai这是很多机器默认 UTC 时间导致任务跑错时间的关键所在。from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.triggers.cron import CronTrigger from datetime import datetime def job_news_daily(): print(f[{datetime.now()}] 开始执行日报任务) try: news_items fetch_news() market_items [fetch_price(sku) for sku in [SKU001, SKU002]] report generate_report(news_items, market_items) push_to_wecom(report) except Exception as e: # 异常时推送告警而不是静默失败 push_to_wecom(f日报生成失败{e}请检查日志) # 这里建议把完整堆栈写到文件里方便复盘 with open(task_error.log, a, encodingutf-8) as f: f.write(f[{datetime.now()}] {e}\n) print(f[{datetime.now()}] 日报任务结束) if __name__ __main__: scheduler BlockingScheduler(timezoneAsia/Shanghai) scheduler.add_job( job_news_daily, CronTrigger.from_crontab(30 8 * * *), iddaily_report, misfire_grace_time3600, coalesceTrue, ) scheduler.start()这里有两个参数值得展开misfire_grace_time和coalesce。当任务本应执行时机器休眠或调度器被阻塞错过了执行时间misfire_grace_time表示“最多允许迟到多久”如果任务已经延迟超过这个秒数本次就不再执行。coalesce设置为 True意思是如果同一任务被连续错过了很多次恢复后只执行最后一次不积压。这两项配置可以避免长时间宕机后任务一恢复就疯狂跑几十遍补作业的情况。推送函数push_to_wecom的实现并不复杂企业微信群机器人本质就是一个 WebhookPOST 一段 JSON 就能把结果发到群里手机端会立刻收到通知。邮件推送则是用 SMTP 发送 HTML 内容。如果你只想自己看推送到企业微信群是最轻量的方案几行代码搞定不用申请任何额外权限。3.5 完整推送示例企业微信群机器人import requests def push_to_wecom(content): webhook_url https://qyapi.weixin.qq.com/cgi-bin/webhook/send?keyyour-key data { msgtype: markdown, markdown: { content: content } } resp requests.post(webhook_url, jsondata, timeout10) resp.raise_for_status()这个方式我用了很久稳定性和触达率都比邮件高。日常跑完之后还可以把你关注的关键词加进去让模型生成完之后自动告诉你今天有没有相关内容。4. 跑起来以后常见问题与排坑实录4.1 任务没有按时执行先查时区定时任务最常见的翻车现场是“我设了早上 8 点跑结果 8 点根本没动静”。第一步不是改代码而是检查服务器的时区。很多云服务器默认是 UTC 时间比北京时间慢了 8 个小时。你以为的早上 8 点在服务器看来其实是凌晨 0 点调度器可能根本没触发。排查时先执行这条命令date -R如果输出里看到 UTC而不是 CST 或 0800那就说明时区不对。解决办法有两种把系统时区改成 Asia/Shanghai或者在调度器初始化时显式指定timezoneAsia/Shanghai。第二种方式更推荐因为它在代码层面保证了不管部署到哪台机器行为一致。4.2 任务一直失败但没有报错另一种让人崩溃的情况是日志里看不到任何异常但是结果就是没生成。多半原因是你的异常捕获太宽泛把错误吞掉了。很多人写定时任务时习惯在最外层包一个巨大的try...except然后只在日志里打print(e)。如果这个e是IndexError或者KeyError你只看到报错信息也很难定位是哪一行代码的问题。我的习惯是每层函数只做本层该做的事异常先抛出在最外层统一捕获并且把完整堆栈写入 log 文件而不仅仅是str(e)。日志里一定要包含时间戳、任务名、堆栈。这样排查效率能高一个数量级。4.3 大模型输出不稳定JSON 解析老失败这个问题大部分情况下不是模型太笨而是没给模型足够清晰的格式约束。你在 Prompt 里写了“输出 JSON”但模型偶尔会在 JSON 前后加一些解释性文字或者把字段名换成带引号的变体。解决方法是双管齐下第一在 Prompt 末尾明确要求“只输出合法 JSON不要输出任何多余解释”第二解析代码里做兼容处理提取第一个{到最后一个}之间的子串再解析。为了从根上减少这类问题我会把 temperature 调低同时所有字段名设计得尽量简单、无歧义避免出现“list”和“列表”这种混用描述。如果你用的模型本身支持结构化输出直接启用那个功能效果更稳定。4.4 价格盯梢任务误报太多价格监控一开始很容易做成“每天都在报”。因为价格接口里的数值可能本身就包含微小的波动比如 3.999 和 4.001这种差异毫无意义但你如果只做“有变化就提醒”那系统每天都会推送一堆噪音用不了三天你就会关掉推送。要让盯价任务有价值需要加两层判断第一先对齐单位和小数精度第二设定变化阈值比如价格波动超过 5% 才提醒或者在连续下跌三天时才提醒。只在有意义的变化发生时通知才是一条程序员应该给自己的工作流。4.5 数据源接口改版这是长期课题接口改版无法避免要保证它在出问题时能第一时间让你感知。让所有采集函数严格检查返回的关键字段一旦字段缺失就主动抛出异常并触发告警推送。宁可让任务失败到明面上也不要让脚本默默返回一个空列表然后生成一个没有内容的“假简报”。具体做法是把 schema 校验写进采集层。比如 JSON 返回里必须包含title和url字段否则抛KeyError。这样推送的失败告警能告诉你“资讯接口结构变了需要改动代码”而不是等每天跑完才发现没有新内容。5. 再进一步把 AI 定时任务从“能用”做成“好用”5.1 构建增量记忆避免每次从零开始我现在做的版本里加了一层叫“历史上下文”的缓存。逻辑很简单每次任务执行完把生成结果里的关注点和价格基线存到本地 SQLite 文件里。下一次执行时把上次的结果塞进 Prompt 作为参考让大模型基于“昨天的结论 今天的新数据”输出而不是每次都像失忆一样从零开始。这个改动带来的体验提升非常明显。原本日报只是“今天的摘要汇总”加了历史上下文之后日报里会自然出现“对比昨日今日重点增加了……”这类延续性表达更有日报的感觉。5.2 多任务共用一个调度器如果你后续想同时跑日报、周报、价格监控多个任务不需要部署多个服务。在一个 Python 进程里注册多个 job用不同的 id 和 cron 表达式区分即可。把每个任务的配置抽到独立配置文件或 JSON 里代码里用循环注册 job以后想新增任务只改配置文件就行。5.3 预留手动触发入口定时任务再稳总有你想“立刻跑一次”的时候。我习惯在每个主函数入口加一个参数支持手动触发比如python daily_report.py --now或者给主脚本加一个简单命令行参数判断到--now就跳过等待定时触发、立即执行一次。这个小开关在调试模型输出、验证新数据源时特别有用不用每次改完代码都等第二天的定时点。调试成本降下来你才更愿意持续迭代这个系统。5.4 关于失败的重试策略别无脑重试大模型 API 超时可以重试但你自己的数据源接口返回 4xx重试十次也是同样的错误结果。我的经验是给不同错误类型配置不同的重试策略网络超时和 5xx 做最多三次指数退避重试参数错误、权限错误这类问题不重试直接告警人工处理。把重试策略按异常类型拆分才不会让任务卡在一个根本无解的错误上反复空转。写在最后这套系统我自己跑了三个多月最大的感触不是省了多少重复劳动力而是每天早上打开手机看到的已经不是一堆未读消息和数据而是一份别人已经帮你梳理好的结论。定时任务负责准时AI 负责思考中间那层胶水代码归根结底考验的就是你有没有把“稳定的调度、清晰的数据、靠谱的生成”这三件事想明白。如果你也想搭一套我最真诚的建议是先别追求大而全挑一个小场景比如“每天早上生成一条行业摘要”就够了花一个周末把链路跑通再逐步往里面加数据源、加判断规则、加推送渠道。等它真正在你生活中稳定跑起来的时候你会回来感谢当初那个愿意动手的自己。
返回列表