)
1. 为什么我要用 Hermes Agent 搭一个资讯看板AI 行业的信息密度已经到了一个夸张的程度早上刷到的模型发布中午可能就被新版本盖过去了。我自己的痛点很具体——关注了十几个 RSS 源和公告页但真正有价值的内容往往被淹没在噪音里等看到的时候已经是旧闻。Hermes Agent 是 NousResearch 出的开源智能体框架它最大的特点是模型无关你可以让它跑在任意兼容 OpenAI 接口的后端上不绑定某一家。这一点对做资讯看板特别关键因为采集、摘要、分类这三步都需要频繁调用模型如果被单一厂商锁死成本和可用性都会很被动。这套方案要解决的问题就三个把分散的信息源统一采集、用智能体做摘要和分类、用一个能实时刷新的看板展示出来。适合谁适合想自己动手跑通一条采集→智能处理→可视化链路的开发者Python 基础够用就行不需要懂前端。技术栈我选了 Hermes Agent DuckDB Streamlit。DuckDB 是嵌入式 OLAP 数据库单文件存储、零配置不用单独起服务Streamlit 是纯 Python 的数据应用框架写几十行就能出一个交互式大屏。三者组合下来整套系统没有需要运维的中间件。下面我会把完整链路拆开先讲 TaoToken 的统一 Key 怎么配这是让 Hermes Agent 能稳定调用模型的前置再给可复制的 config.toml 骨架和采集代码然后是看板渲染和验证动作最后是我踩过的几个坑。2. TaoToken 前置给 Hermes Agent 配一条统一的模型通道Hermes Agent 本身不提供模型它需要一个兼容 OpenAI 协议的 API 端点。你可以直接对接各家厂商但那样每换一个模型就要改一次 Key 和 base_url采集脚本里到处散落着密钥维护起来很烦。我的做法是用 TaoToken 做统一通道一个 Key、一个 base_url后面想换模型只改 model 字段就行。官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 。具体操作分三步。第一步进控制台创建 Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 在 API Keys 页面点新建复制出来的字符串就是后面要用的凭证。第二步确认你要用的模型名可以在模型对话页面先试跑一句地址 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 输入一句你好看能不能正常返回能返回说明 Key 和通道都没问题。第三步把 Key 写进环境变量不要硬编码在脚本里。注意Key 一旦泄露要立刻在控制台吊销重建。采集脚本建议从环境变量读取别图省事写死在代码里。如果你后面要做长期编码或者跑 Agent 任务可以考虑 Coding Plan地址 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 它更适合高频调用的场景。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 参数细节以文档为准。3. 可复制配置config.toml 骨架与采集流水线3.1 config.toml 骨架Hermes Agent 的配置我拆成两块模型通道和采集源。下面这份可以直接复制把 api_key 换成你自己的。# config.toml — Hermes Agent 资讯看板配置 [model] # TaoToken 统一通道 base_url https://taotoken.net/api api_key ${TAOTOKEN_API_KEY} # 从环境变量读取 model deepseek-v3 # 想换模型只改这一行 temperature 0.1 max_tokens 500 timeout 30 [database] path ai_hot_news.db [agent] # 单次采集每个源最多取几条 max_items_per_source 10 # 每条之间的间隔避免请求过密 sleep_seconds 1 [sources] # 键是显示名值是 RSS 地址按需增删 IT之家AI https://feedx.net/rss/ithome.xml Hacker News https://hnrss.org/frontpage HuggingFace论文 https://huggingface.co/papers.rss环境变量这样设Linux/Mac 用export TAOTOKEN_API_KEY你的KeyWindows 用set TAOTOKEN_API_KEY你的Key或者写进.env文件用 python-dotenv 加载。3.2 依赖安装python -m venv ai-hot-env # Windows ai-hot-env\Scripts\activate # Linux/Mac source ai-hot-env/bin/activate pip install duckdb streamlit plotly pandas requests feedparser python-dotenv版本上duckdb 建议 1.0 以上streamlit 1.30 以上feedparser 6.0 以上。这几个库都比较稳定装最新版一般没问题。3.3 采集流水线代码核心逻辑是读配置 → 拉 RSS → 交给 Hermes Agent 做摘要和分类 → 写进 DuckDB。下面这份hermes_pipeline.py可以直接跑。import os import json import time import hashlib import tomllib from datetime import datetime import feedparser import requests import duckdb from dotenv import load_dotenv load_dotenv() with open(config.toml, rb) as f: CFG tomllib.load(f) MODEL_CFG CFG[model] API_KEY os.environ.get(TAOTOKEN_API_KEY, ) PROMPT_TEMPLATE 你是AI热点情报分析师。分析下面的原始内容只输出JSON。 原始内容{raw} 要求 1. 提取 title、summary100字内、category 2. category 只能是模型发布、产品更新、行业动态、观点技巧、论文研究 3. 无价值则返回 {{skip: true}} 输出格式{{title:...,summary:...,category:...}} def fetch_rss(url): feed feedparser.parse(url) return [ { title: e.get(title, ), link: e.get(link, ), summary: e.get(summary, ), } for e in feed.entries[: MODEL_CFG.get(max_items_per_source, 10)] ] def call_agent(text): prompt PROMPT_TEMPLATE.format(rawtext[:2000]) resp requests.post( f{MODEL_CFG[base_url]}/v1/chat/completions, headers{ Authorization: fBearer {API_KEY}, Content-Type: application/json, }, json{ model: MODEL_CFG[model], messages: [{role: user, content: prompt}], temperature: MODEL_CFG.get(temperature, 0.1), max_tokens: MODEL_CFG.get(max_tokens, 500), }, timeoutMODEL_CFG.get(timeout, 30), ) content resp.json()[choices][0][message][content] content content.replace(json, ).replace(, ).strip() return json.loads(content) def init_db(path): conn duckdb.connect(path) conn.execute( CREATE TABLE IF NOT EXISTS news_items ( id VARCHAR PRIMARY KEY, title VARCHAR, summary VARCHAR, url VARCHAR, source VARCHAR, category VARCHAR, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) return conn def save(conn, item): item_id hashlib.md5(item[url].encode()).hexdigest()[:16] conn.execute( INSERT OR IGNORE INTO news_items (id, title, summary, url, source, category) VALUES (?, ?, ?, ?, ?, ?), (item_id, item[title], item[summary], item[url], item[source], item[category]), ) def main(): conn init_db(CFG[database][path]) saved 0 for name, url in CFG[sources].items(): print(f采集: {name}) for entry in fetch_rss(url): raw f标题: {entry[title]}\n摘要: {entry[summary][:500]} try: result call_agent(raw) except Exception as e: print(f 调用失败: {e}) continue if result.get(skip): continue save(conn, { title: result[title], summary: result.get(summary, ), url: entry[link], source: name, category: result.get(category, 未分类), }) saved 1 print(f [] {result[title][:40]}) time.sleep(CFG[agent][sleep_seconds]) total conn.execute(SELECT COUNT(*) FROM news_items).fetchone()[0] print(f本次新增 {saved} 条累计 {total} 条) conn.close() if __name__ __main__: main()这里有个细节base_url 我写的是https://taotoken.net/api拼接时加了/v1/chat/completions。如果你的通道路径不同以接入文档为准别照抄路径。4. 看板渲染与验证请求4.1 Streamlit 看板dashboard.py负责把 DuckDB 里的数据渲染出来。核心是 KPI 卡片、分类饼图、趋势柱状图和最新列表四块。import streamlit as st import duckdb import plotly.express as px st.set_page_config(page_titleAI 实时资讯看板, layoutwide) conn duckdb.connect(ai_hot_news.db, read_onlyTrue) total conn.execute(SELECT COUNT(*) FROM news_items).fetchone()[0] today conn.execute( SELECT COUNT(*) FROM news_items WHERE DATE(created_at)CURRENT_DATE ).fetchone()[0] st.title(AI 实时资讯看板) st.caption(f累计 {total} 条 | 今日新增 {today} 条) c1, c2, c3 st.columns(3) c1.metric(总资讯, total) c2.metric(今日新增, today) c3.metric(分类数, conn.execute( SELECT COUNT(DISTINCT category) FROM news_items).fetchone()[0]) cats conn.execute( SELECT category, COUNT(*) cnt FROM news_items GROUP BY category ).fetchdf() if not cats.empty: st.plotly_chart(px.pie(cats, namescategory, valuescnt, hole0.5), use_container_widthTrue) items conn.execute( SELECT title, summary, source, category, created_at FROM news_items ORDER BY created_at DESC LIMIT 20 ).fetchdf() for _, r in items.iterrows(): st.markdown(f**{r[title]}** \n{r[category]} | {r[source]} \n{r[summary][:150]}) conn.close()4.2 验证动作先跑采集确认能写入数据python hermes_pipeline.py正常输出会逐条打印[] 标题最后给出累计条数。如果全是调用失败先回去检查 Key 和 base_url。再起看板streamlit run dashboard.py --server.port 8501浏览器打开http://localhost:8501你应该能看到顶部三个指标卡、分类饼图和最新资讯列表。如果指标卡显示 0说明采集没写进去回到上一步查。想单独验证模型通道是否通可以直接发一条请求curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d {model:deepseek-v3,messages:[{role:user,content:回复OK}]}返回里有choices字段就说明通道正常。这一步能帮你快速区分是通道问题还是脚本问题。5. 本篇常见错排查报错一KeyError: choices。多半是返回体里带了 error 字段常见原因是 Key 无效或余额不足。先看resp.json()的完整内容别只看异常信息。如果提示鉴权失败去控制台确认 Key 状态。报错二json.decoder.JSONDecodeError。模型返回的内容里混了说明文字json.loads解析不了。我在 prompt 里已经强调只输出JSON但偶尔还是会带 markdown 代码块标记所以代码里做了replace(json,)处理。如果还不行加一层正则提取第一个{到最后一个}之间的内容。报错三DuckDB 报database is locked。看板用了read_onlyTrue打开采集脚本是读写模式两个进程同时开同一个文件会冲突。解决办法是采集和看板错开或者采集时先关掉看板。DuckDB 单文件不适合高并发写这点要注意。报错四RSS 采集返回空列表。有些源对请求头有要求或者地址已经失效。先用浏览器打开那个 RSS 地址确认还能访问再检查 feedparser 的bozo标志。失效的源直接从 config.toml 里删掉。报错五Streamlit 页面空白。通常是ai_hot_news.db文件不存在read_onlyTrue打开失败。先跑一次采集生成数据库文件再起看板。报错六中文乱码。RSS 源编码不统一时会出现。feedparser 一般能自动处理如果还有问题在fetch_rss里对 summary 做一次编码转换。6. 后续怎么扩展信息源扩展最简单直接在 config.toml 的[sources]里加一行就行不用改代码。模型切换也只改model字段比如换成别的模型名通道和 Key 都不用动。数据量涨起来之后DuckDB 可以加索引加速时间范围查询CREATE INDEX idx_created_at ON news_items(created_at);定时采集的话Linux 用 crontab 每 30 分钟跑一次*/30 * * * * cd /path/to/project source ai-hot-env/bin/activate python hermes_pipeline.py pipeline.log 21Windows 就用任务计划程序建一个 bat 文件调用虚拟环境里的 python 即可。如果你想把采集频率提上去、或者让 Agent 做更复杂的多步任务可以看看 Coding Plan地址 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 它更适合长期高频的调用场景。接入参数和路径细节以 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 上的文档为准别照抄我这里的示例路径。整套跑下来从环境准备到看板上线大概半小时。真正花时间的是调 prompt——分类的边界、摘要的长度这些需要根据你自己的信息源反复试。我建议先把max_items_per_source设小一点跑通链路再放量。