欢迎光临
我们一直在努力

【开源】RssHarnessForPython:一个基于 LangGraph + RSSHub 的时效问答 Agent 系统

最近我把一个断断续续做了一段的项目整理后开源了:GitHub – Nexknit/RssHarnessForPython: Time-sensitive Q&A agent。这是个回答“有时效性”问题的 Agent 系统,核心思路一句话:不调用搜索引擎,而是让子 Agent 自主决策——走哪个平台的哪条 RSSHub 路由,直达源站抓取,失败就换路,抓完读库汇总成稿。

写这篇文章,是想把它为什么这么设计、踩了哪些坑讲清楚。

背景:先想清楚“时效性问题”为什么不该交给搜索引擎

像“某某平台最近有什么值得注意的动态”“某芯片型号对比的消息”这类问题,关键属性是时效。传统搜索/索引方案大致是:爬全网 → 建索引 → 等爬虫按周期刷新。这层“搬运工”决定了:你再快,也快不过源站自己。

而 RSS 的思路反过来了——RSSHub 路由是源站维护者(或社区)预声明的结构化端点。你要微博热榜,它就有一条 /weibo/keyword/… 路由;你要 36kr 快讯,它就是 /36kr/newsflash。源站热榜多快,RSS 端点多快,时效是贴着源站走的,中间没有“等索引刷新”的时延。

所以这个项目从一开始就砍掉了“搜索引擎/索引层”,核心变成一个很纯粹的问题:一个能自己决定去哪个平台、走哪条路由,并在每一步失败时知道该重试还是该换路的子 Agent。

它跟搜索引擎”区别

  • 没有爬全网建索引那层搬运工,时效贴着源站实时走;
  • RSSHub 会把上游 404、源站要登录、缺浏览器统一包装成自己的 503,子图会透传真实原因并据此分类(语义错 vs 瞬时错),而不是盲目重试;
  • 架构:双服务 monorepo + 自建 RSSHub + 共享 PostgreSQL

    你(CLI 三色 / HTTP :8001)


    agent 域 services/agent 编排 + 智能(LangGraph)
    主对话图:START→chat_agent→chat_tools→(回边)→chat_agent→END
    唯一工具 = 终结性 rss 子图
    控制平面:MCP(SSE over HTTP)


    rss 域 services/rss 抓取 + 数据
    抓取(并发 GET rsshub:1200{path} → feedparser → 去重入库)
    路由发现(持 RSSHub 官方目录 ~1689 平台 / 7.9MB → 归一化 catalog)


    RSSHub(chromium-bundled) 源站热榜/tag/关键词 → 结构化 RSS 端点


    数据平面:共享 PostgreSQL(rss 写 feeds/items · agent 读 + 回写 chat_threads/turns)

    四个服务的分工:

    服务目录角色
    agent services/agent LangGraph 编排、LLM、CLI/HTTP(:8001)
    rss services/rss 抓取解析入库、路由发现、MCP 服务端(:8000)
    rsshub 自建 RSS 源生成(chromium-bundled 变体,:1200)
    db PostgreSQL 16,双服务共享(:5432)

    两个服务分开走:数据平面走共享 PostgreSQL,控制平面走 MCP(agent 作为 MCP 客户端调 rss 域的工具)。

    主对话图:agent 工具循环

    这个项目的主对话图是一个带回边的循环:START → chat_agent → chat_tools →[回边]→ chat_agent → END。agent 先决策要不要抓 → 工具跑 rss 子图、把成品答案包装成 ToolMessage 回填 → 回边再进 agent,让模型看到真实结果后决定是直接作答还是继续调,直到直接作答或 max_agent_steps 护栏触发。

    这里有个刻意的设计:唯一工具是“终结性”的 rss 子图,它直接产出成品答案,避免了“子图抓完 → 外层再转述一遍、白烧一轮 token”的浪费。

    另一个值得读的点是查询改写显式化:子图的唯一入参 query 必填,模型每次调用前要把用户意图改写成一句自包含的、脱离历史也能看懂的问题。外层真正喂进子图的,就是这句改写——意图理解被收敛到一个明确接口上,好测也好调。

    会话层面:雪花 thread_id 建线程,Postgres checkpointer(AsyncPostgresSaver)支持跨轮续跑,每轮问答 + usage 都会落记录表(chat_threads/chat_turns),可回查。

    RSS 子图:四个节点 + 一张跳转表

    RssSummaryFetchGraph.run(question) 每次 run 都会现编一个全新的 StateGraph(stateless、全新 UsageLedger),四个节点:

    platform(平台查证) → route(选路) → fetch(抓取) → summarize(收口,唯一出口)

    每级都可能失败,失败怎么走,由 router 决定,这也是子图“会回退”的根据:

    • fetch 后任一路径成功 → 直接去 summarize(数据到手即收口,不恋战);
    • 全挂且都是语义错(404、源站要登录这类)→ 换路由(对语义错重试没意义);
    • 是瞬时错(超时/限流)→ 同批路径重试,fetch_retry=2 次仍败 → 换路由 → 还选不出 → 回 platform 拓宽 → 预算耗尽收口。

    所有路径共享一个预算闸 router_budget(默认 8):前三个节点每走一步就自增一次,预算耗尽一律落 summarize——连“诚实失败”也走这个出口,保证图一定终结,不会空转。

  • 子图私有状态键全部带 rsfg_ 前缀,累加键用 Annotated[list, operator.add]——future-proof,永远不会和父图的裸键撞车;
  • 所有 IO 收口在一个 RuntimeContext(llm_text/search_platform/fetch_paths/read_items)——这是唯一的接缝,测试全靠替换它,所以才能做到下文说的“全离线测试”。
  • 实时追踪

    想做的效果:CLI 里实时看到子图内部——改写后的问题、每步尝试/配额/决策、这步 token 花了多少。但 LangGraph 1.2.x 的 callback 只给到节点边界(节点体 trace=False,end 不带节点输出),拿不到“这步选了啥”。

    于是实现上是回调语义、自持实现:run() 保持 ainvoke 不变;CLI 回合注册 live sink(ContextVar,service.chat 用 try/finally 设/清)时,在 add_node 边界 + router.decide 处包一层 RsfgLiveTracer,每完成一步上抛一条保留事件 {"rsfg_live": event}。关键约束是:没有 sink 时零开销、行为逐字节不变——事件永不进 state/message/checkpoint/usage,HTTP 和离线路径完全不受影响。

    CLI 里长这样:

    │ 改写后查询:36氪 最近有什么值得注意的快讯
    │ 平台探测 #1/8 目录查实 → 选路
    │ · platform.propose in=520 out=64 reasoning=0 cache=0
    │ 抓取 #3/8 数据到手 → 收口总结
    │ ✓ [r1] /36kr/newsflash → 拉到 3 条
    │ 收口总结 #4/8 读库 3 条 → 成稿 1280 字

    一次真实运行回放:错误是怎么被如实处理的

    README 里有一段 2026-09-08 的真实运行回放(交互 CLI 问芯片对比这类时效题),不是演示数据,重点是看错误处理、诚实度与预算编排:

    • 两次子图调用、共 24.8s(含跨平台多路并发抓取 + 总结),usage in 15513 / out 3250 / cache 1536,答案带完整时间戳、无一条臆造;
    • 同一批回执里,失败被如实透传并分类:
      • ✗ /ithome/tag/麒麟9050 → RSSHub 503:FetchError 404(自编 tag 名查无此家 → 语义错)
      • ✗ /zhihu/xhu/topic/… → 401(源站要登录)
      • ✓ /weibo/keyword/… → 拉到 10 条、✓ /bilibili/vsearch/… → 拉到 16 条(部分成功照常收口)
    • 数据不足以定量回答时,最终回复会明说“没有权威第三方测评/没有直接对比数据,观点存分歧”,给出建议,不编数字——材料里没有的时间,就不写。

    工程质量:测试全离线,照样全绿

    agent 域 83 项测试、rss 域 66 项测试,ruff clean(py312,line-length 100)。重要的是这些测试全程无网络、无 Postgres、无真 LLM:主对话图用 InMemorySaver + 假 model/subgraph;子图用 context_factory 注入鸭子 ctx,跑真编译的 StateGraph 离线端到端;DB 是 in-memory sqlite + StaticPool。也就是说 CI 里不用配任何密钥就能跑(CI 已经配好,见仓库 .github/workflows/ci.yml)。

    快速开始

    # 一键拉起完整栈(db + rss + agent + rsshub)
    make up
    进入交互式对话 CLI
    make cli
    子图直跑,跳过外层对话,直接看拆平台/选路/抓取/汇总
    cd services/agent && uv run python -m app.graph.service "小米 fold 最新消息"
    HTTP 入口
    curl -X POST http://localhost:8001/api/v1/agent/chat

    -H 'content-type: application/json'

    -d '{"question":"36氪 最新快讯"}'

    环境提示:LLM 走 OpenAI 兼容接口(当前接 DeepSeek),密钥只放根目录 .env(已 gitignore,不会进镜像);主对话 checkpoint 依赖共享 PostgreSQL;RSSHub 用 chromium-bundled 变体,bilibili/weibo 这类要浏览器渲染的路由普通镜像会一律 503。

    已知边界

  • 部分成功即收口——fetch 一轮里只要任一路径成功就进 summarize,同轮失败路径没有第二次被重选的机会,这是“答了旧的没答新的”的机制根源,修法(在 summarize 前加一个能同时看到 OK 与失败路径的裁决点)已想清;
  • 失败分类仍粗——RSSHub 把上游 404 也包装成 503,classify_status 对 503 先当瞬时错同批重试一轮才换路由,属半修;
  • 开源信息

    • 仓库:RssHarnessForPython — GitHub – Nexknit/RssHarnessForPython: Time-sensitive Q&A agent that plans its own RSSHub routes to reach source sites, retries or falls back on failure, and summarizes a shared store into timestamped briefs. LangGraph, Python, dual-service monorepo. · GitHub
    • 协议:MIT(可商用、可二开,保留版权声明即可)
    • 文档:根目录 README(英文为主,顶部可切中文 README.zh-CN.md);services/rss/docs/route-index-optimization.md 有一篇路由索引的性能优化实测笔记,感兴趣可以读
    • 目录速查:代码在 services/agent(LangGraph 编排)与 services/rss(抓取 + MCP 服务端)两个服务下,关键文件坐标都在 README 的「目录速查」一节

    如果你想看一个“Agent 如何把回退做得显式、把 IO 收敛成可测接缝、把预算做成硬终结保障”的例子,欢迎 star 和提 issue。有想聊的架构取舍,也可以直接在仓库 Discussions 或评论区找我。

    赞(0)
    未经允许不得转载:171主机测评 » 【开源】RssHarnessForPython:一个基于 LangGraph + RSSHub 的时效问答 Agent 系统
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址