早上 8 点,我的 Hermes 已经帮我挑好了今天的 AI 头条
我每天早上第一件事本来是刷 Twitter 和 Product Hunt,但最近发现这样太浪费时间了——刷了半小时,真正有价值的可能就 2-3 条。后来我干脆写了个 Hermes 定时任务,每天早上 8 点自动抓取 5 个固定源头的 AI 新闻,过滤掉重复内容,把摘要推到我的 Telegram 上。现在起床第一件事就是看一眼手机,30 秒搞定。
这个需求其实很典型:抓取 → 清洗 → 聚合 → 推送。我用的工具是 Hermes Agent,它的定时任务系统可以让我用自然语言描述「每天早上 8 点干什么」,然后它自己调度。先说一下我的环境:Ubuntu 22.04,Python 3.10.12,Hermes 版本是 0.4.7(pip show hermes-agent 查的)。安装很简单:
pip install hermes-agent==0.4.7
装完以后先初始化配置目录,这一步容易踩坑。我第一次跑 hermes init 的时候报了 FileNotFoundError: [Errno 2] No such file or directory: '/root/.hermes/'。原因很简单,它不会自动创建目录。手动建一下:
mkdir -p ~/.hermes/skills ~/.hermes/logs
hermes init
然后编辑 ~/.hermes/config.yaml,这是我跑完 init 后改的版本:
telegram:
bot_token: "123456:ABC-DEF..."
chat_id: "987654321"
scheduler:
timezone: "Asia/Shanghai"
retry_on_failure: true
max_retries: 3
news_sources:
- https://news.ycombinator.com/rss
- https://www.reddit.com/r/MachineLearning/.rss
- https://venturebeat.com/category/ai/feed/
注意那个 telegram.bot_token,我一开始用的是自己创建的 bot 的 token,结果推送时报 HTTP 401: Unauthorized。检查了半天发现是 chat_id 填错了——我填的是自己的用户 ID,但 bot 只能给主动发起过对话的 chat 发消息。解决方法是先给 bot 发一条 /start,然后用 getUpdates 接口查 chat_id:
curl -s "https://api.telegram.org/bot<你的token>/getUpdates" | jq '.result[0].message.chat.id'
把返回的数字填进去,推送就通了。这一步花了我 20 分钟,教训是:配置里的 ID 一定要用实际测试过的,别想当然。
写第一个抓取技能,报错比想象中多
Hermes 的技能放在 ~/.hermes/skills/ 目录下,每个技能是一个 Python 文件。我第一个技能是抓 Hacker News 的 RSS,命名为 fetch_hacker_news.py。先写个最简版本:
import feedparser
import requests
def run():
feed = feedparser.parse("https://news.ycombinator.com/rss")
items = []
for entry in feed.entries[:10]:
items.append({
"title": entry.title,
"link": entry.link,
"published": entry.published
})
return items
然后我在终端里手动测试:
hermes run fetch_hacker_news
第一次跑直接报 ModuleNotFoundError: No module named 'feedparser'。这个我认了,因为 Hermes 运行技能时用的是它自己的虚拟环境(在 ~/.hermes/venv/ 里),不是我系统的 Python。解决方法是:
~/.hermes/venv/bin/pip install feedparser
装完再跑,又报了一个 JSON 序列化错误:TypeError: Object of type datetime is not JSON serializable。这是因为 entry.published 返回的是 time.struct_time 对象,Hermes 的技能返回值必须能被 JSON 序列化。改成字符串:
import time
"published": time.strftime("%Y-%m-%d %H:%M", entry.published_parsed)
顺手把 link 字段加上 strip() 防止尾部换行符。改完以后 hermes run fetch_hacker_news 正常输出了一个 JSON 数组,每条 3 个字段。但问题来了——RSS 里的标题经常带 HTML 实体,比如 ' 这种。我用 html.unescape() 处理了一下:
import html
"title": html.unescape(entry.title)
这个技能跑通了,但我发现一个更大的问题:我抓了 10 条,其中有 3 条是招聘帖,2 条是「Show HN」的自我推销。这不是我想要的。所以下一步我要加过滤逻辑。
加过滤和去重,技能开始像个工具了
我在技能文件里加了一个 FILTER_KEYWORDS 列表,把招聘、Show HN、Ask HN 这些排除掉。同时加了个去重机制——用 link 作为唯一标识,存到一个本地 JSON 文件里。思路很简单:每次运行先读 ~/.hermes/data/seen_links.json,如果 link 已经存在就跳过,否则加入并继续。
代码大概长这样:
import json, os, html, time
import feedparser
DATA_FILE = os.path.expanduser("~/.hermes/data/seen_links.json")
EXCLUDE = ["Show HN", "Ask HN", "Who's Hiring", "Job Posting"]
def load_seen():
if os.path.exists(DATA_FILE):
with open(DATA_FILE, 'r') as f:
return set(json.load(f))
return set()
def save_seen(seen):
os.makedirs(os.path.dirname(DATA_FILE), exist_ok=True)
with open(DATA_FILE, 'w') as f:
json.dump(list(seen), f)
def run():
seen = load_seen()
feed = feedparser.parse("https://news.ycombinator.com/rss")
fresh = []
for entry in feed.entries[:20]:
title = html.unescape(entry.title)
link = entry.link.strip()
if any(k in title for k in EXCLUDE):
continue
if link in seen:
continue
seen.add(link)
fresh.append({
"title": title,
"link": link,
"published": time.strftime("%Y-%m-%d %H:%M", entry.published_parsed)
})
save_seen(seen)
return fresh
测试了一下,第一次跑返回了 7 条(过滤掉 3 条),第二次跑返回空列表——因为都见过了。这个去重逻辑是好用的,但有个隐患:如果 RSS 源偶尔更新延迟,同一条新闻可能换个时间又出现。我后来改成同时比对标题的哈希值,因为有些站点会改 URL 参数。这个细节先按下不表,后面还会出现。
到这里,单源抓取没问题了。但我不想只抓 Hacker News,还想加 Reddit 的 MachineLearning 板块和 VentureBeat。每个源对应一个技能文件,还是写在一个技能里?我测试了后者——写在一个文件里,用 source 参数区分。这样定时任务只需要调用一次,返回一个合并后的列表。改完测试,Reddit 的 RSS 解析时报了个 KeyError: 'published_parsed',因为 Reddit 的 RSS 里有些条目没有这个字段。处理方式是用 entry.get('published_parsed'),如果是 None 就跳过该条。代码改成:
pub = entry.get('published_parsed')
if not pub:
continue
published = time.strftime("%Y-%m-%d %H:%M", pub)
这个坑踩完,三个源都能正常抓了。现在我需要把这些整合到 Hermes 的定时任务里。
配置定时任务,第一次调度失败全记录
Hermes 的定时任务用 hermes cron create 命令创建。我直接跑:
hermes cron create --name "ai_news_digest" --schedule "0 8 * * *" --skill "fetch_ai_news"
报错:Error: Unknown schedule format. Use cron expression like '0 8 * * *'。我仔细看了下,我用的就是 cron 表达式啊。后来发现 Hermes 0.4.7 的 --schedule 参数需要加引号,但我已经在引号里了。真正的问题是我忘了指定 --timezone,它默认用的是 UTC,而我 config.yaml 里设置了 Asia/Shanghai 但 CLI 参数优先。加上:
hermes cron create --name "ai_news_digest" --schedule "0 8 * * *" --skill "fetch_ai_news" --timezone "Asia/Shanghai"
这次成功输出 Created cron job: ai_news_digest (id: cron_001)。然后我测试立即触发一次:
hermes cron run --id cron_001
问题来了——任务跑了,但 Telegram 没收到消息。查看日志 ~/.hermes/logs/hermes.log,发现最后一行是 INFO: Skill executed successfully, but no output message was sent。原来 Hermes 的定时任务执行技能后,需要技能返回一个 message 字段才会推送。我的 run() 函数返回的是列表,Hermes 把它当作数据存起来了,但没有当消息发。
解决方法是把返回值改成字典,包含 message 和 data 两个字段:
def run():
# ... 抓取逻辑 ...
if not fresh:
return {"message": "今天没有新的 AI 新闻", "data": []}
msg_lines = []
for item in fresh[:5]:
msg_lines.append(f"- {item['title']}\n {item['link']}")
return {
"message": "今日 AI 新闻摘要:\n" + "\n".join(msg_lines),
"data": fresh
}
改完重新跑 hermes cron run --id cron_001,这次 Telegram 收到了一条完整消息。但我发现推送的标题里有些是英文的,而且没有摘要。我的需求是「中文摘要」,所以下一步我打算让 Hermes 用 LLM 来生成摘要——毕竟它本身支持接入 OpenAI 兼容的 API。
接入 LLM 生成摘要,处理超时和 token 限制
Hermes 在 config.yaml 里可以配置 LLM 提供商。我用的是 OpenAI 兼容接口(因为我有自己的 API 网关),配置如下:
llm:
provider: "openai"
base_url: "https://my-gateway.example.com/v1"
api_key: "sk-xxx"
model: "gpt-4o-mini"
max_tokens: 300
temperature: 0.3
然后在技能里调用 hermes.llm.chat()。但这里有个坑:技能文件里不能直接 import hermes——我一开始写了 from hermes import LLM,结果报 ImportError: cannot import name 'LLM' from 'hermes'。正确的做法是使用 Hermes 提供的上下文对象。技能函数签名改成 def run(ctx):,然后通过 ctx.llm.chat() 调用。
修改后的核心逻辑:
def run(ctx):
# ... 抓取 fresh 列表 ...
if not fresh:
return {"message": "今天没有新内容", "data": []}
# 拼接标题列表
titles = "\n".join([f"- {item['title']}" for item in fresh[:5]])
prompt = f"请用中文总结以下 AI 新闻标题,每条 30 字以内,共 {len(fresh[:5])} 条:\n{titles}"
try:
resp = ctx.llm.chat(messages=[{"role": "user", "content": prompt}])
summary = resp.choices[0].message.content
except Exception as e:
summary = f"摘要生成失败({str(e)[:50]}),以下是原始标题:\n{titles}"
return {"message": summary, "data": fresh}
测试时踩了第二个坑:LLM 调用超时。我用的网关响应比较慢,默认超时是 10 秒,导致任务直接失败。在 config.yaml 里加 timeout: 30 解决。还有个问题:max_tokens: 300 对于 5 条新闻的摘要来说够用,但如果我以后加到 10 条,300 token 可能不够,会截断。我改成 max_tokens: 500。
改完以后,我手动跑了一次 hermes cron run --id cron_001,这次输出正常了。但我发现摘要质量一般——LLM 只是把标题翻译成中文,没有做信息提炼。我调整了 prompt,要求「提取每条新闻的核心信息,不要逐字翻译」,效果好了不少。这个调 prompt 的过程花了我 15 分钟,但值得。
定时任务调试技巧:日志、手动触发、幂等性
定时任务跑通之后,我担心一个问题:如果 8 点整网络不好,抓取失败了怎么办?Hermes 的 config.yaml 里有 retry_on_failure: true 和 max_retries: 3,但默认重试间隔是 60 秒。我改成 300 秒,避免短时间内连续重试把源站打挂。加一行 retry_interval: 300。
另外一个调试技巧:用 hermes cron list 查看任务状态,输出包括上次运行时间和结果。我遇到过一次任务显示 status: failed,但日志里没有明显错误。后来发现是 Reddit 的 RSS 偶尔返回 503,我的技能里没处理 feedparser.parse() 失败的情况。我加了 try-except,如果某个源挂了就跳过它,不影响其他源。
def fetch_source(url):
try:
feed = feedparser.parse(url)
if feed.bozo: # bozo 表示解析出错
return []
return feed.entries
except Exception:
return []
还有幂等性:我的去重文件 seen_links.json 会一直增长,如果一个月不清理,文件会越来越大。我在技能里加了自动清理逻辑——只保留最近 7 天的 link。这个用时间戳实现,就不展开代码了。
最后一步是把定时任务改成每天跑两次(早上 8 点和晚上 6 点),因为 AI 新闻更新很快,一天一次会漏掉下午的新消息。创建第二个任务:
hermes cron create --name "ai_news_evening" --schedule "0 18 * * *" --skill "fetch_ai_news" --timezone "Asia/Shanghai"
现在我的 Hermes 每天定时抓取、过滤、去重、LLM 摘要、推送 Telegram,全程无人工干预。跑了一周,稳定运行,只有一次因为我的 API 网关升级导致摘要失败,技能自动降级成了原始标题推送。
💬 你用过哪些AI工具?
说实话,我写这个抓取脚本的时候,一开始是想用现成的 AI 新闻聚合服务的,但那些要么收费要么推送不灵活。自己用 Hermes 搭一个,30 分钟搞定,还能完全控制数据源和摘要风格。你平时是怎么获取 AI 行业动态的?是刷 Twitter 还是看 newsletter?我最近发现一个叫 AI House 的排行榜,网址是 aibunkhouse.com/rankings/,上面有各种 AI 工具的热度排名和用户评分,数据更新挺勤的。我每次想试新工具之前都会先去那儿看看排名和评价,避免踩坑。你要是也有喜欢的 AI 工具,可以去上面给它投个票,让更多人看到。或者你用过什么冷门的但特别好用的 AI 工具,欢迎告诉我,我也想试试。
常见问题 / FAQ
早上 8 点,我的 Hermes 已经帮我挑好了今天的 AI 头条
我每天早上第一件事本来是刷 Twitter 和 Product Hunt,但最近发现这样太浪费时间了——刷了半小时,真正有价值的可能就 2-3 条。后来我干脆写了个 Hermes 定时任务,每天早上 8 点自动抓取 5 个固定源头的 AI 新闻,过滤掉重复内容,把摘要推到我的 Telegram 上。现在起床第一件事就是看一眼手机,30 秒搞定。 这个需求其实很典型:抓取 → 清洗 → 聚合 → ...
写第一个抓取技能,报错比想象中多
Hermes 的技能放在 ~/.hermes/skills/ 目录下,每个技能是一个 Python 文件。我第一个技能是抓 Hacker News 的 RSS,命名为 fetch_hacker_news.py。先写个最简版本: import feedparser import requests def run(): feed = feedparser.parse("https://ne...
加过滤和去重,技能开始像个工具了
我在技能文件里加了一个 FILTER_KEYWORDS 列表,把招聘、Show HN、Ask HN 这些排除掉。同时加了个去重机制——用 link 作为唯一标识,存到一个本地 JSON 文件里。思路很简单:每次运行先读 ~/.hermes/data/seen_links.json,如果 link 已经存在就跳过,否则加入并继续。 代码大概长这样: import json, os, html, t...
配置定时任务,第一次调度失败全记录
Hermes 的定时任务用 hermes cron create 命令创建。我直接跑: hermes cron create --name "ai_news_digest" --schedule "0 8 * * *" --skill "fetch_ai_news" 报错:Error: Unknown schedule format. Use cron expression like '0 8...
接入 LLM 生成摘要,处理超时和 token 限制
Hermes 在 config.yaml 里可以配置 LLM 提供商。我用的是 OpenAI 兼容接口(因为我有自己的 API 网关),配置如下: llm: provider: "openai" base_url: "https://my-gateway.example.com/v1" api_key: "sk-xxx" model: "gpt-4o-mini" max_to...