1. 从收藏夹吃灰说起:这个项目到底要解决什么
GitHub 的 Star 功能大概是程序员用得最多、也最容易被忽视的一个按钮。看到有意思的项目随手点个星,日积月累下来,收藏夹里躺着几百上千个仓库,真到要用的时候却一个都想不起来。我自己就有这个毛病,收藏夹里从前端框架到数据库工具,从爬虫脚本到机器学习教程,什么都有,但每次需要找某个具体工具时,还是习惯性地去搜索引擎重新搜一遍——收藏夹等于白收藏了。
这个项目的核心思路很直接:用 AI Agent 自动整理 GitHub 收藏夹。具体来说,就是让一个智能体定期读取你 Star 过的仓库列表,抓取每个仓库的描述、语言、主题标签、Star 数、最近更新时间等元信息,然后自动完成分类、打标签、生成摘要、识别过时项目、甚至给出“这个项目值不值得深入看”的判断。最终输出一份结构化的清单,你可以按分类浏览,也可以直接搜索关键词找到对应的仓库。
它解决的是三个层面的问题。第一层是信息过载:收藏夹本质是一个无序列表,GitHub 官方只提供了按语言和按 Star 时间排序,没有自定义分类能力。第二层是记忆衰减:你收藏一个项目时的上下文——当时为什么觉得它有用、打算用它做什么——过两周就忘干净了。第三层是维护成本:手动整理几百个仓库,光是逐个点开看描述就要花掉一整个下午,没人愿意干这种活。
适合看这篇内容的人有三类。一是收藏夹已经超过 200 个仓库、明显感到管理失控的开发者;二是想学 AI Agent 实战、但苦于找不到真实可用场景的人;三是平时用 GitHub 做技术调研、需要定期梳理技术选型清单的团队技术负责人。哪怕你只是想给自己的收藏夹做个“体检”,这套方法也能直接抄作业。
2. 整体方案设计:为什么这样搭
2.1 核心架构选型与理由
整个系统拆成四层:数据采集层、AI 处理层、存储层、展示层。这个分层不是拍脑袋定的,而是根据实际约束倒推出来的。
数据采集层负责从 GitHub 拉取收藏列表和仓库详情。这里有个关键决策:用 GitHub 官方的 REST API 还是 GraphQL API。REST API 的/user/starred端点返回的是仓库的基本信息,但如果你想要每个仓库的 topics、README 摘要、最近提交时间,就得对每个仓库再发一次请求,N 个仓库就是 N+1 次调用。GraphQL API 可以在一次请求里把仓库列表和每个仓库的详细字段一起拿回来,对于收藏夹这种“批量读取”场景,GraphQL 明显更合适。我实测下来,500 个仓库用 GraphQL 分页拉取,大概 10 次请求就能搞定,而 REST 方式要发 500 次以上,光是等网络往返就要好几分钟。
AI 处理层是核心。这里的选择是用大模型做分类和摘要,而不是用规则引擎。原因很简单:规则引擎需要你预先定义好所有分类标签,但收藏夹里的项目类型是开放式的,今天收藏一个 Rust 写的 CLI 工具,明天收藏一个用 Python 做的数据分析库,规则根本覆盖不全。大模型的优势在于它能理解仓库描述的自然语言含义,自动归纳出合理的分类,比如“前端框架”“数据库驱动”“DevOps 工具”“学习资源”这些标签,不需要你提前枚举。
存储层用 SQLite 就够了。收藏夹的数据量级在几百到几千条,SQLite 单文件存储、零配置、支持全文搜索,完全够用。没必要上 PostgreSQL 或 MongoDB,那是过度设计。展示层可以是一个静态 HTML 页面,也可以是一个简单的命令行工具,甚至直接输出 Markdown 文件。我倾向于生成 Markdown,因为可以直接丢进 Obsidian 或 Notion 里做二次整理。
2.2 为什么用 Agent 而不是一次性脚本
有人可能会问:这不就是一个脚本干的事吗,为什么要套个 Agent 的壳?区别在于决策的灵活性。一次性脚本的逻辑是固定的:拉数据、调模型、写文件。但实际整理收藏夹时,你会遇到各种需要判断的情况。比如某个仓库已经三年没更新了,脚本只能标记“过时”,但 Agent 可以进一步判断:这个项目是已经完成使命的稳定库(比如一个不再需要新功能的工具),还是真的被废弃了?再比如两个仓库功能高度重叠,Agent 可以识别出来并建议你保留哪个、归档哪个。
Agent 的另一个价值是可扩展的工具调用。整理收藏夹不只是分类,还可能涉及:检查仓库是否有安全漏洞、对比同类项目的 Star 增长趋势、生成学习路线建议。这些任务需要调用不同的工具(比如漏洞数据库查询、趋势数据 API),Agent 架构天然支持动态选择工具,而脚本要做到这一点就得写一大堆 if-else。
2.3 数据流与处理流程
完整的数据流是这样的:首先通过 GitHub GraphQL API 拉取当前用户的所有 Star 仓库,字段包括名称、描述、主要语言、topics、Star 数、fork 数、创建时间、最后推送时间、是否归档、README 前 500 字。然后对每个仓库做预处理:清洗描述文本、提取关键词、计算“活跃度分数”(综合最后推送时间和 Star 增长)。接着把预处理后的数据分批送给大模型,让模型输出分类标签和一句话摘要。最后把结果写入 SQLite,并生成一份按分类组织的 Markdown 清单。
这里有个细节值得展开:为什么要分批而不是一次性把所有仓库丢给模型。大模型的上下文窗口虽然大,但一次塞几百个仓库的描述,不仅 token 成本高,而且模型在长上下文里的分类一致性会下降——前面分类用了一个标签,后面可能换了个近义词。分批处理(每批 20 到 30 个仓库)可以保证同一批内的分类标准一致,同时通过 prompt 里携带“已有分类列表”来维持跨批次的一致性。
3. 核心细节解析:从 API 调用到 Prompt 设计
3.1 GitHub 数据采集的关键参数
用 GraphQL 拉取收藏列表时,有几个参数直接决定了后续处理的效率。第一个是分页大小,GitHub GraphQL 单次请求最多返回 100 条记录,所以first: 100是标配。第二个是字段选择,不要贪多,只取后续需要的字段。我试过把 README 全文拉下来,结果 500 个仓库的响应体超过了 10MB,解析起来很慢。后来改成只取 README 的前 500 个字符,足够模型判断项目类型了。
第三个是速率限制的处理。GitHub API 对未认证请求限制很严,认证后 GraphQL 的配额是每小时 5000 个点。每个仓库查询大概消耗 1 到 2 个点,所以 500 个仓库一次拉取完全在配额内。但如果你频繁运行,就需要做缓存——把已经拉取过的仓库数据存到本地,下次只拉取新增的 Star。判断新增的方式是记录上次拉取的时间戳,只查询starredAt晚于该时间戳的仓库。
# GraphQL 查询示例(简化版) query = """ { viewer { starredRepositories(first: 100, after: %s, orderBy: {field: STARRED_AT, direction: DESC}) { pageInfo { hasNextPage endCursor } edges { starredAt node { nameWithOwner description primaryLanguage { name } repositoryTopics(first: 10) { nodes { topic { name } } } stargazerCount forkCount createdAt pushedAt isArchived object(expression: "HEAD:README.md") { ... on Blob { text } } } } } } } """注意:README 的获取方式在不同仓库里可能不同,有的用 README.md,有的用 README.rst,还有的放在 docs 目录下。稳妥的做法是先查
HEAD:README.md,如果返回空再尝试其他常见路径。不过对于分类任务来说,仓库描述加上 topics 已经提供了足够的信息,README 只是锦上添花。
3.2 Prompt 设计的核心要点
Prompt 的质量直接决定了分类的准确率。我踩过的坑是:一开始只给模型一句“请对这些 GitHub 仓库进行分类”,结果模型返回的分类五花八门,有的按编程语言分,有的按用途分,还有的按 Star 数分,完全没有统一标准。
后来改成结构化 Prompt,包含四个部分:角色定义、分类体系说明、输出格式要求、示例。角色定义让模型知道自己是一个“技术项目分类助手”;分类体系说明里给出推荐的分类维度(如“前端开发”“后端框架”“数据库”“DevOps”“机器学习”“学习资源”“工具软件”等),但允许模型在遇到无法归类的项目时创建新分类;输出格式要求用 JSON,每个仓库包含category、tags、summary、maturity四个字段;示例给出一到两个完整的输入输出对,让模型模仿。
这里的关键是maturity字段的设计。我把它定义为一个枚举值:active(最近半年有更新)、stable(一年内有更新但功能已完善)、stale(超过一年未更新)、archived(已归档)。这个字段比单纯的“最后更新时间”更有信息量,因为它结合了项目的实际状态。模型在判断时会参考仓库描述里是否有“deprecated”“no longer maintained”等关键词,也会看 Star 数和 fork 数的比例——如果一个项目 Star 很高但 fork 很少,通常说明它是个资源列表或教程,而不是代码库。
3.3 分类一致性的保障机制
分批处理带来的最大问题是分类不一致。比如第一批里模型把某个项目归为“Web 框架”,第二批里类似的项目却被归为“后端开发”。解决这个问题有两个手段。
第一个手段是维护一个动态分类列表。每批处理前,把当前已经确定的分类列表附在 prompt 里,并明确告诉模型“优先使用已有分类,只有在确实无法归类时才创建新分类”。这个列表随着批次推进不断增长,后续批次的分类会自然向已有分类靠拢。
第二个手段是后处理归一化。所有批次处理完后,把所有分类名拿出来做一次聚类,把语义相近的分类合并。比如“前端框架”和“Web 前端”可以合并,“机器学习”和“ML”可以合并。这一步可以用简单的字符串相似度算法,也可以再调一次模型做判断。我实测下来,经过这两步处理,分类的一致性可以做到 90% 以上,剩下的 10% 主要是那些本身就跨领域的项目,归到哪个分类都说得通。
4. 实操过程:从零搭建你的收藏夹整理 Agent
4.1 环境准备与依赖安装
先明确技术栈:Python 3.10 以上、GitHub 个人访问令牌、一个大模型 API(OpenAI、Claude 或国内可用的模型服务都行)、SQLite。Python 依赖主要是requests(或httpx)用于发 HTTP 请求,sqlite3是标准库自带,tqdm用于显示进度条。
GitHub 令牌的获取路径是:GitHub 设置 → Developer settings → Personal access tokens → Tokens (classic) → Generate new token。需要的权限范围是read:user和public_repo。如果你收藏了私有仓库,还需要repo权限。令牌生成后存到环境变量里,不要硬编码在代码中。
# 设置环境变量(Linux/macOS) export GITHUB_TOKEN="ghp_xxxxxxxxxxxx" export LLM_API_KEY="sk-xxxxxxxxxxxx" # Windows PowerShell $env:GITHUB_TOKEN="ghp_xxxxxxxxxxxx" $env:LLM_API_KEY="sk-xxxxxxxxxxxx"提示:令牌一旦生成就要妥善保管,不要提交到 Git 仓库。建议在项目根目录加一个
.env文件,用python-dotenv加载,同时把.env加入.gitignore。
4.2 数据拉取与本地缓存实现
数据拉取模块的核心逻辑是:先查本地数据库里已有的仓库列表和最后更新时间,然后只向 GitHub 请求新增的 Star。这样第一次运行会慢一些(要拉全部),后续运行就很快了。
import sqlite3 import requests import os from datetime import datetime def init_db(): conn = sqlite3.connect("stars.db") conn.execute(""" CREATE TABLE IF NOT EXISTS repos ( full_name TEXT PRIMARY KEY, description TEXT, language TEXT, topics TEXT, stars INTEGER, forks INTEGER, created_at TEXT, pushed_at TEXT, is_archived INTEGER, starred_at TEXT, category TEXT, tags TEXT, summary TEXT, maturity TEXT, updated_at TEXT ) """) conn.commit() return conn def fetch_starred(cursor=None): token = os.environ["GITHUB_TOKEN"] headers = {"Authorization": f"Bearer {token}"} query = """...""" # 上面定义的 GraphQL 查询 variables = {"cursor": cursor} resp = requests.post( "https://api.github.com/graphql", json={"query": query, "variables": variables}, headers=headers, timeout=30 ) resp.raise_for_status() return resp.json()拉取的时候要注意分页。GraphQL 返回的pageInfo.hasNextPage为 true 时,把endCursor作为下一次请求的cursor继续拉,直到hasNextPage为 false。每拉一页就写入数据库,这样即使中途失败,下次也能从断点继续。
4.3 AI 分类与摘要生成
分类模块的核心是把仓库数据组织成模型能理解的格式,然后解析模型返回的 JSON。我用的 prompt 模板大致如下:
PROMPT_TEMPLATE = """ 你是一个技术项目分类助手。请对以下 GitHub 仓库进行分类和摘要。 已有分类列表:{existing_categories} 对每个仓库,输出以下字段: - category: 从已有分类中选择,或创建新分类 - tags: 3-5 个关键词标签 - summary: 一句话中文摘要,不超过 50 字 - maturity: active / stable / stale / archived 之一 仓库列表: {repos_json} 请以 JSON 数组格式返回,不要包含其他内容。 """repos_json里每个仓库只保留必要字段:名称、描述、语言、topics、Star 数、最后推送时间、是否归档。描述如果太长就截断到 200 字。模型返回的 JSON 要做校验,如果解析失败就重试一次,重试时在 prompt 里加上“上次返回的格式有误,请确保返回合法的 JSON 数组”。
4.4 结果存储与 Markdown 生成
所有仓库处理完后,从数据库里按分类查询,生成 Markdown 文件。每个分类一个二级标题,下面用表格列出仓库名称、摘要、标签、成熟度。表格的好处是信息密度高,一眼能扫完。
def generate_markdown(conn): categories = conn.execute( "SELECT DISTINCT category FROM repos WHERE category IS NOT NULL ORDER BY category" ).fetchall() lines = ["# GitHub 收藏夹整理\n"] for (cat,) in categories: lines.append(f"## {cat}\n") lines.append("| 仓库 | 摘要 | 标签 | 状态 |") lines.append("|------|------|------|------|") rows = conn.execute( "SELECT full_name, summary, tags, maturity FROM repos WHERE category = ?", (cat,) ).fetchall() for name, summary, tags, maturity in rows: lines.append(f"| [{name}](https://github.com/{name}) | {summary} | {tags} | {maturity} |") lines.append("") with open("stars_report.md", "w", encoding="utf-8") as f: f.write("\n".join(lines))生成的 Markdown 可以直接在 GitHub 上预览,也可以导入到笔记软件里。我习惯把它放到 Obsidian 的 vault 里,配合 Dataview 插件做动态查询,比如“列出所有 maturity 为 stale 的仓库”,方便定期清理。
5. 常见问题与排查技巧实录
5.1 API 调用失败的排查思路
最常见的问题是 GitHub API 返回 401 或 403。401 通常是令牌无效或过期,检查环境变量是否设置正确。403 一般是速率限制,GraphQL 的响应头里会有X-RateLimit-Remaining和X-RateLimit-Reset,前者为 0 时说明配额用尽,后者是配额重置的时间戳。处理方式是等待重置,或者用多个令牌轮换(但要注意不要违反 GitHub 的使用条款)。
另一个坑是网络超时。国内访问 GitHub API 有时会遇到连接超时,尤其是在拉取大量数据时。解决办法是设置合理的超时时间(比如 30 秒)并加重试逻辑。重试时用指数退避,第一次等 1 秒,第二次等 2 秒,第三次等 4 秒,最多重试 3 次。如果还是失败,就把当前进度保存下来,下次从断点继续。
5.2 模型输出格式不稳定的处理
大模型返回的 JSON 偶尔会带 markdown 代码块标记(比如json 和),或者在中途插入解释性文字。解析前先做清洗:去掉代码块标记,找到第一个[和最后一个],截取中间部分再解析。如果解析还是失败,就把原始返回内容存到日志里,人工检查是什么问题。
还有一种情况是模型返回的仓库数量和输入不一致。比如输入 20 个仓库,模型只返回了 18 个。这通常是因为模型在长列表里“偷懒”了。解决办法是减小批次大小,从 30 降到 20 甚至 15。批次越小,模型遗漏的概率越低,但总调用次数会增加。我实测下来,每批 20 个是比较平衡的选择。
5.3 分类结果不符合预期的调整方法
如果发现模型把明显该归为一类的项目分到了不同类别,先检查 prompt 里的分类列表是否足够清晰。比如“工具”这个分类太宽泛,模型可能会把 CLI 工具、GUI 工具、在线工具都往里塞。改成“命令行工具”“桌面软件”“在线服务”这样更具体的分类,模型的判断会准确很多。
另一个调整手段是在 prompt 里加入负向示例。比如告诉模型“不要把教程类项目归入框架类,教程类项目应该归入学习资源”。负向示例比正向示例更能纠正模型的系统性偏差。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| API 返回 401 | 令牌无效或未设置 | 检查环境变量,重新生成令牌 |
| API 返回 403 | 速率限制用尽 | 等待重置,或减少请求频率 |
| 请求超时 | 网络不稳定 | 增加超时时间,加重试逻辑 |
| 模型返回非 JSON | 输出格式不稳定 | 清洗返回内容,重试并强调格式要求 |
| 分类不一致 | 批次间标准漂移 | 维护动态分类列表,后处理归一化 |
| 仓库遗漏 | 批次过大 | 减小批次大小到 15-20 |
| README 拉取失败 | 文件路径不标准 | 只依赖描述和 topics,跳过 README |
实操心得:第一次运行建议先用 20 个仓库做小规模测试,确认整个流程跑通、模型输出格式稳定后,再扩大到全量收藏夹。这样即使出问题,排查成本也低得多。
6. 进阶玩法:让 Agent 真正“下地干活”
6.1 自动识别重复与相似项目
收藏夹里经常有功能重叠的项目,比如同时收藏了三四个 JSON 解析库。Agent 可以通过对比仓库描述和 topics 来识别相似项目,并在报告里标注“与 XXX 功能相似,建议二选一”。实现方式是在分类完成后,对同一分类下的仓库做两两相似度计算,可以用简单的 Jaccard 相似度(基于 topics 集合),也可以再调一次模型做语义判断。
6.2 生成学习路线建议
对于学习资源类的收藏,Agent 可以根据仓库的难度标签和内容类型,生成一条建议的学习顺序。比如先看入门教程,再看实战项目,最后看源码解析。这个功能需要在 prompt 里让模型额外输出一个difficulty字段(beginner / intermediate / advanced),然后按难度排序。
6.3 定期自动运行与变更通知
把整个流程封装成一个脚本,用系统的定时任务(Linux 的 cron 或 Windows 的任务计划程序)每周运行一次。每次运行后对比上次的结果,如果有新增仓库或分类变化,就生成一份变更摘要。变更摘要可以输出到控制台,也可以发到自己的笔记软件或消息工具里。这样收藏夹就变成了一个“活”的知识库,而不是一个只进不出的黑洞。
6.4 与笔记软件的深度集成
生成的 Markdown 文件可以直接放到 Obsidian 的 vault 里,配合 Dataview 插件做动态查询。比如:
TABLE summary, tags, maturity FROM "github-stars" WHERE maturity = "stale" SORT stars DESC这条查询会列出所有标记为 stale 的仓库,按 Star 数排序,方便你决定哪些可以取消收藏。如果你用 Notion,也可以通过 Notion API 把数据同步过去,做成一个可交互的数据库视图。
7. 一些踩坑之后的个人体会
这套方案我从去年开始用,前后迭代了四五个版本,最大的体会是:不要追求一次做到完美。第一版只要能拉取数据、能分类、能生成 Markdown 就够了。用起来之后你自然会发现问题,比如分类不够细、摘要太长、某些仓库被遗漏,然后再针对性优化。如果一开始就想设计一个完美的分类体系,大概率会卡在“分类到底怎么定”这一步,迟迟无法落地。
另一个体会是模型的选择比 prompt 的优化更重要。我试过用不同规模的模型跑同一套 prompt,小模型在分类一致性上明显差一截,经常出现同一批里类似项目分到不同类别的情况。如果预算允许,建议用中等规模以上的模型做分类任务,省下来的调试时间远比模型费用值钱。
最后说一个容易被忽视的点:收藏夹整理不是目的,用起来才是。整理完之后,我给自己定了个规矩:每周花 10 分钟浏览一遍新生成的报告,挑一个之前收藏但一直没看的项目,花半小时快速过一遍它的 README 和示例代码。这样收藏夹才真正变成了一个持续产生价值的知识库,而不是一个自我安慰的“稍后阅读”坟场。