SurfSense Reddit 专业子代理:结构化帖子、评论与社区舆情数据的实时抓取与对比分析
【免费下载链接】SurfSenseOpen-source NotebookLM alternative. Research the open web with live data(Reddit, YT, IG, TikTok, Indeed, Google Search, Maps etc) through one platform, API or MCP server. Join our Discord: https://discord.gg/ejRNvftDp9项目地址: https://gitcode.com/GitHub_Trending/su/SurfSense
导读
SurfSense 的多智能体聊天系统内置了多位"专业子代理"(builtin sub-agent),其中reddit子代理专门负责从 Reddit 抓取结构化数据:帖子(标题、正文、评分、点赞率、评论数、所属 subreddit、作者、flair、时间戳)、评论线程以及社区/用户元数据。它既支持按关键词搜索发现讨论(可选限定单一 subreddit),也支持直接抓取已知的帖子、subreddit 或用户 URL,并提供排序与时间窗口控制,还能把当前抓取结果与本次对话中早前获得的 Reddit 结果做对比。读完本文,你将掌握该子代理的触发条件、三种数据获取路径、全部抓取参数、底层源码调用链以及输出契约。
定位与职责边界:Reddit 讨论的专属入口
根据 description.md 的定义,这个子代理的职责非常聚焦——"pulls structured Reddit data",即拉取结构化的 Reddit 数据,包括:
- 帖子(posts):标题、正文、score(评分)、upvote ratio(点赞率)、评论数、subreddit、作者、flair、时间戳;
- 评论线程(comment threads):每条帖子的评论树;
- 社区与用户元数据(community/user metadata):subreddit 与用户维度的信息。
与之配套的 system_prompt.md 明确说明了它何时被调用:只要任务是了解"人们在 Reddit 上对一个话题说了什么",包括从某个 subreddit 或用户收集帖子/评论、发现讨论线程、读取社区情绪(community sentiment),都交给它。典型触发语包括:
- "search Reddit for X"(在 Reddit 搜索 X)
- "what does r/X say about Y"(r/X 社区对 Y 怎么看)
- "find Reddit posts/threads about X"(查找关于 X 的 Reddit 帖子/线程)
- "top posts in r/X"(r/X 的置顶热帖)
- 以及对比本对话中早前的 Reddit 结果
同时,它被明确限制为只管 Reddit:
- 普通网页内容 → 交给 web 爬虫子代理(web crawling specialist);
- Google 搜索结果 → 交给 Google Search 子代理;
- YouTube 内容 → 交给 YouTube 子代理。
这种职责划分保证了 supervisor 代理在委派任务时不会出现能力重叠,也让每个子代理的提示词(prompt)可以高度专业化。
三种数据获取路径:搜索、URL 直抓与社区浏览
从 schemas.py 的ScrapeInput可以看到,reddit子代理暴露了三种互不排斥的数据获取方式(三者至少提供其一,校验逻辑见_require_a_source模型校验器):
- 关键词搜索(
search_queries):运行 Reddit 站内搜索,每个查询最多返回max_items条结果;配合community参数可把搜索限定到单一 subreddit(例如"python",不带r/前缀)。 - URL 直抓(
urls):直接抓取一个帖子 URL、subreddit URL(/r/<name>)、用户 URL(/user/<name>)甚至 Reddit 搜索 URL,原样作为抓取目标。 - 社区浏览(
community单独使用):只传community而不传search_queries时,抓取该 subreddit 的帖子列表(listing)。
单次调用的来源上限为MAX_REDDIT_SOURCES = 20(即urls与search_queries总数不超过 20),以约束同步请求的扇出规模;单次返回的条目硬上限为MAX_REDDIT_ITEMS = 100(max_items的le=100约束)。
抓取参数全解析
下表汇总了ScrapeInput的全部字段及其语义(来自 schemas.py):
| 参数 | 类型/默认值 | 说明 |
|---|---|---|
urls | list[HttpUrlStr],默认[] | 要抓取的 Reddit URL:帖子、/r/<name>、/user/<name>或搜索 URL。与search_queries/community至少提供其一 |
search_queries | list[str],默认[] | 在 Reddit 上运行的搜索词,每个查询最多返回max_items条结果;可用community限定 subreddit |
community | str \| None,默认None | subreddit 名称(不带r/),用于限定search_queries;不传search_queries时抓取其列表 |
sort | RedditSort,默认"new" | 结果排序:relevance、hot、top、new、rising、comments |
time_filter | RedditTime \| None,默认None | 仅对top/controversial排序生效的时间窗口:hour、day、week、month、year、all |
include_nsfw | bool,默认True | 是否包含标记为 18+(NSFW)的帖子 |
skip_comments | bool,默认False | 跳过评论树抓取(更快,只取帖子/列表) |
max_items | int,默认10,范围1~100 | 所有来源合计返回的最大条目总数 |
max_posts | int,默认10,范围>=0 | 每个 subreddit/用户/搜索目标最多抓取的帖子数 |
max_comments | int,默认10,范围>=0 | 每篇帖子最多抓取的评论数(0= 不抓评论) |
post_date_limit | str \| None,默认None | ISO 日期;只返回晚于该日期的帖子(用于增量抓取) |
comment_date_limit | str \| None,默认None | ISO 日期;只返回晚于该日期的评论(用于增量抓取) |
值得注意的是max_items的默认值只有 10,而它是跨所有来源的硬性总上限。如果任务是"找出 N 篇帖子",就必须把max_items和max_posts都调到大于 N 的值(并预留出离题命中的余量),否则"上限小于目标"的调用永远不可能满足任务要求。estimated_units属性把它作为预检(pre-flight)计费闸门的最大可计费条目数返回。
排序与时间窗口:控制结果的口径
sort与time_filter是控制抓取口径的关键参数:
sort的六个取值(RedditSort枚举):relevance(相关度)、hot(热门)、top(最高分)、new(最新)、rising(上升中)、comments(评论最多)。默认是new。time_filter的六个窗口(RedditTime枚举):hour、day、week、month、year、all。注意它只对top与controversial排序有意义——这就是为什么 system_prompt.md 里的 playbook 会说"抓取某个 subreddit 的列表时,用sort(hot/top/new/rising)并配合time_filter取 top/controversial"。
playbook 还给出了一条重要的检索经验:主题发现要用宽查询而非精确短语。当目标是"发现求 X 推荐/替代方案的帖子"时,应该用不带引号的宽泛查询加上多种措辞(例如"X alternative"、"alternative to X"、"app like X"),并使用sort=relevance;而带引号的精确短语与sort=new是"精度工具",会漏掉绝大多数匹配。这直接呼应了sort参数中relevance与new的定位差异。
评论阅读与体积控制
评论是 Reddit 舆情分析的核心。子代理在读取评论情绪时有明确的策略:
- 需要评论时:保持
skip_comments为false(默认),并调高max_comments; - 只要帖子、追求速度时:把
skip_comments设为true; - 控制总量时:用
max_items做总上限、max_posts控制每个目标的帖子数、max_comments控制每篇帖子的评论数。
由于每个返回条目都对应一个计费单位(见下文 billing 部分),体积控制同时也是成本控制——这正是estimated_units与billable_units两个属性存在的意义。
与早前结果的对比分析
description.md特别强调该子代理"compares fresh Reddit results against earlier findings in this chat"。这一能力在 system_prompt.md 中被实现为对比 playbook:
- 拉取当前结果;
- 与本次对话中早前工具调用已返回的 Reddit 结果对照;
- 报告具体的增量(delta):新增(added)、移除(removed)、评分/排名变化(score/rank changes)。
这类对比需求来自诸如"和上次相比有什么变化""哪些帖子是新的""热度走势如何"之类的委派任务。需要说明的是,该机制依赖的是同一对话上下文中早前工具结果的留存(run reader 可读取已存储的抓取输出),因此对比的"基准线"就是本对话历史中已出现的 Reddit 结果。
源码级实现:从子代理构建到能力执行
子代理的装配
agent.py 中的build_subagent()展示了内置子代理的统一装配模式:
- 通过
load_tools()加载工具,并可与外部 MCP 工具合并; - 通过
read_md_file(__package__, "description")读取本目录的description.md作为子代理的路由描述(供 supervisor 判断何时委派); - 通过
read_md_file(__package__, "system_prompt")读取system_prompt.md作为系统提示词; - 最终用
pack_subagent(...)打包成SurfSenseSubagentSpec交给 deepagents 框架运行。
也就是说,description.md与system_prompt.md不只是文档,它们分别是 supervisor 的路由决策依据和子代理的行为准则,属于运行时代码的一部分——这就是本仓库"以文档驱动子代理行为"的设计。
工具与权限
tools/index.py 定义了NAME = "reddit"、RULESET = Ruleset(origin=NAME, rules=[]),并通过build_capability_tools()把REDDIT_SCRAPE这一个能力动词(_CI_VERBS)暴露为 LangChain 工具。当前rules为空,即该子代理不附加额外的权限规则,只依赖能力注册系统。
能力的注册与计费
definition.py 将reddit.scrape注册为一个正式能力(Capability):
- 输入/输出模式分别为
ScrapeInput/ScrapeOutput; - 执行器由
build_scrape_executor()构建; - 计费单位为
BillingUnit.REDDIT_ITEM,按抓取条目数计费(配置项REDDIT_SCRAPE_MICROS_PER_ITEM控制每条的微信用额度); docs_url指向/docs/connectors/native/reddit。
ScrapeOutput.billable_units明确"一个返回条目 = 一个计费单位"(len(self.items))。
执行器与错误处理
executor.py 中的execute()是参数映射的核心:它把ScrapeInput逐字段映射到底层专有爬虫的RedditScrapeInput(startUrls、searches、searchCommunityName、sort、time、includeNSFW、skipComments、maxItems、maxPostCount、maxComments、postDateLimit、commentDateLimit),调用专有平台的scrape_reddit,并在执行前后通过emit_progress发出进度事件("Resolving Reddit targets" → "Scraped N item(s)"),供前端实时展示。
错误处理上有一个值得注意的设计:Reddit 是仅匿名访问的爬虫,一旦被拒绝访问抛出RedditAccessBlockedError,由于没有凭据可重试,执行器会将其映射为ForbiddenError(错误码REDDIT_ACCESS_BLOCKED),与 google_maps 能力的SignInRequiredError → ForbiddenError映射方式保持一致,让上层 API 层可以统一处理权限类错误。
输出契约与失败策略
system_prompt.md 为子代理规定了严格的输出契约(output contract),要求只返回一个 JSON 对象,字段包括:
{ "status": "success" | "partial" | "blocked" | "error", "action_summary": "string", "evidence": { "findings": ["每条发现一句话,最多10条"], "sources": ["每个发现对应的 Reddit URL,每个URL只列一次"], "confidence": "high" | "medium" | "low" }, "next_step": "string | null", "missing_fields": "string[] | null", "assumptions": "string[] | null" }其中findings按"每条帖子/评论/增量一条"组织,禁止粘贴原始载荷,且必须由真实抓取结果支撑、绝不补位;sources与findings同数量上限,每个 Reddit URL 只列出一次。
对应的失败策略(failure policy)同样严格:
- 请求信息不足(没有可用的查询词、community 或 URL)→ 返回
status=blocked并列出缺失字段; - 工具失败→ 返回
status=error并附简洁的恢复建议next_step; - 没有可用证据→ 返回
status=blocked,同时给出更窄的查询或仍需的抓取范围。
此外,安全策略要求:证据不完整或互相冲突时明确报告不确定性,绝不把未经验证的说法当作事实陈述;工具策略要求只使用可用工具列表内的工具,且只报告工具输出中实际存在的结果——严禁编造标题、URL、分数、作者或评论文本。这种"宁可 blocked 也不虚构"的约束,保证了 supervisor 合成最终答案时的证据可信度。
实战要点速查
- 发现讨论:用
search_queries,需要限定社区时加community(如"python");做主题发现用宽查询 +sort=relevance的多种措辞。 - 抓取社区列表:只传
community,调sort(hot/top/new/rising),需要历史窗口时配time_filter。 - 抓取已知目标:把帖子/subreddit/用户 URL 放进
urls直抓。 - 读评论情绪:保持
skip_comments=false并调高max_comments;只要帖子就把skip_comments=true提速。 - 控制体积与成本:
max_items是跨来源硬上限(默认仅 10),按目标数任务需同时调大max_items与max_posts;每个返回条目计费一个REDDIT_ITEM单位。 - 对比增量:对比基准来自同一对话早前 Reddit 工具结果,报告 added/removed/score 变化。
- 遵守边界:网页内容走 web 爬虫子代理、Google 结果走 Google Search 子代理、YouTube 走 YouTube 子代理,
reddit子代理只处理 Reddit 本身。
延伸阅读
- 子代理路由描述与系统提示词:description.md、system_prompt.md
- 子代理装配实现:agent.py、tools/index.py
- 能力注册与 I/O 契约:definition.py、schemas.py
- 执行器与错误映射:executor.py
【免费下载链接】SurfSenseOpen-source NotebookLM alternative. Research the open web with live data(Reddit, YT, IG, TikTok, Indeed, Google Search, Maps etc) through one platform, API or MCP server. Join our Discord: https://discord.gg/ejRNvftDp9项目地址: https://gitcode.com/GitHub_Trending/su/SurfSense
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考