1. 为什么我要折腾 GitHub 收藏夹自动整理这件事
GitHub 的 Star 功能大概是所有开发者用得最频繁、也最容易被忽视的一个功能。你看到一篇不错的开源项目,点个 Star;刷到某个工具库觉得以后可能用得上,点个 Star;同事在群里甩了个链接,顺手也 Star 一下。一年下来,收藏夹里躺着三五百个项目是常态,多的上千也不稀奇。
问题是,这些 Star 一旦攒起来,基本就等于进了黑洞。GitHub 自带的 Star 列表只支持按时间排序,顶多给你一个搜索框,你想按语言筛选、按用途分类、按活跃度排序,统统做不到。更别提很多人 Star 完就再也没打开过,等到真正需要某个轮子的时候,翻半天翻不到,最后只能重新去搜,搜到的还是同一个项目,然后再 Star 一次——收藏夹里出现重复项,这种事我干过不止一回。
我自己的账号里当时有 800 多个 Star,横跨前端、后端、运维、AI、爬虫、各种乱七八糟的工具。每次想找点东西,都得靠记忆去搜关键词,效率极低。后来我试过手动整理,建了几个 List,分了大概几十个项目就放弃了——太累了,而且新 Star 的项目还是会源源不断地堆进来,手动维护根本跟不上。
所以我就想,能不能用 AI Agent 把这活儿自动化掉。核心思路很简单:定时拉取我的 Star 列表,让 AI 读每个项目的描述和 README,自动打标签、分类、生成摘要,然后写回 GitHub 的 List 或者输出成一份结构化的 Markdown 清单。这样我既不用手动整理,又能随时按分类找到想要的项目。
这篇文章就是我把这套东西从零搭起来、踩了一堆坑之后的完整记录。适合有基本 Python 能力、用过 GitHub API、对 AI Agent 有初步了解的读者。如果你只是想找个现成工具用,那可能得再等等,目前我没看到特别成熟的方案;但如果你想自己搭一套,或者想理解 AI Agent 在真实场景里怎么落地,这篇应该能给你不少参考。
2. 整体方案设计与技术选型思路
2.1 需求拆解:到底要自动化哪些环节
在动手写代码之前,我先把整个流程拆成了几个独立的环节,每个环节单独考虑用什么方案最合适。
第一个环节是数据获取。需要从 GitHub 拿到我所有的 Star 项目,包括项目名、描述、主要语言、Star 数、最后更新时间、Topics 等元信息。这部分用 GitHub 官方的 REST API 就能搞定,不需要 AI 介入。
第二个环节是内容理解。光靠项目描述那一句话,很多时候判断不出这个项目到底是干嘛的。比如一个项目描述写的是 "A fast build tool",你根本不知道它是给前端用的还是给 Rust 用的。所以需要抓取 README 的前若干行,让 AI 结合描述和 README 一起判断。
第三个环节是分类与打标。这是 AI 真正发挥价值的地方。我需要它根据项目内容,给出一个主分类(比如"前端框架"、"数据库"、"AI 工具")、若干标签(比如"TypeScript"、"CLI"、"轻量级"),以及一句话的中文摘要。
第四个环节是结果落地。整理好的数据要能被我方便地使用。我最终选择了两个输出:一个是写回 GitHub 的 List(这样在 GitHub 网页上就能直接看到分类),另一个是生成一份 Markdown 文件(方便本地搜索和备份)。
2.2 为什么选 AI Agent 而不是传统脚本
有人可能会问,分类打标这种事,写一堆 if-else 规则不就行了?比如描述里出现 "react" 就归到前端,出现 "database" 就归到数据库。
我一开始也这么想过,试了之后发现根本行不通。原因有几个:一是项目描述千奇百怪,同一个概念有无数种表达方式,规则覆盖不全;二是很多项目是跨领域的,比如一个用 Rust 写的 Web 框架,你按关键词匹配可能同时命中"Rust"和"Web",到底归哪类?三是规则维护成本高,每次遇到新类型都得加规则,加到最后规则本身就成了一个难以维护的怪物。
AI Agent 的优势在于它能理解语义,而不是匹配关键词。你给它一段描述,它能判断出这个项目的本质用途,哪怕描述里一个关键词都没提到。而且它还能处理模糊情况,比如一个项目既可以算工具也可以算框架,它会根据你的分类体系给出最合理的选择。
当然,AI 也不是万能的。它会有幻觉,会给出不存在的分类,会漏掉重要信息。所以我在方案里加了一层校验和兜底逻辑,这个后面会详细讲。
2.3 技术栈选择与理由
最终我用的技术栈是这样的:
| 组件 | 选型 | 理由 |
|---|---|---|
| 语言 | Python 3.11 | GitHub API 生态成熟,AI SDK 支持好 |
| GitHub 交互 | PyGithub | 封装完善,省去手写 HTTP 请求 |
| AI 调用 | OpenAI 兼容接口 | 通用性强,方便切换不同模型 |
| 数据存储 | SQLite | 轻量,单文件,方便本地跑 |
| 定时调度 | cron | 简单可靠,不需要额外服务 |
| 输出 | Markdown + GitHub List | 兼顾线上查看和本地搜索 |
这里重点说一下为什么用 SQLite 而不是直接每次重新拉取。因为 AI 调用是有成本的,而且同一个项目没必要反复分析。我把每个项目的分析结果缓存到本地数据库里,下次运行时如果项目没更新,就直接用缓存,只有新 Star 的或者 README 有变化的才重新调 AI。这样既省钱又提速。
另外,AI 接口我特意选了 OpenAI 兼容格式,而不是绑定某一家。因为这类任务对模型能力要求不算特别高,用便宜的小模型也能跑,哪天想换模型,改个 base_url 和 model 名字就行,代码不用动。
3. 核心环节的细节拆解与实操要点
3.1 GitHub Star 数据怎么高效拉取
GitHub 的 Star 列表接口是GET /user/starred,支持分页,每页最多 100 条。这里有几个坑要注意。
第一个坑是认证方式。匿名请求有严格的速率限制,每小时只有 60 次,根本不够用。必须用 Personal Access Token(PAT)认证,认证后每小时 5000 次,基本够用。PAT 的权限只需要public_repo或者更小的read:user就够了,不要图省事给全权限。
第二个坑是分页处理。800 个 Star 就是 8 页,你得循环拉取。而且要注意,Star 列表是按 Star 时间倒序排列的,如果你在拉取过程中又 Star 了新项目,可能会导致分页错位。我的做法是先一次性把所有 Star 拉完存到本地,再统一处理,避免边拉边处理。
第三个坑是README 获取。不是所有项目都有 README,有些 README 是图片,有些是超长文档。我的策略是只取 README 的前 2000 个字符,超过部分截断。因为对于分类判断来说,开头部分的信息量已经足够了,全文拉取既慢又浪费 token。
from github import Github import time def fetch_all_stars(token): g = Github(token) user = g.get_user() stars = [] page = 0 while True: batch = user.get_starred().get_page(page) if not batch: break for repo in batch: stars.append({ "full_name": repo.full_name, "description": repo.description or "", "language": repo.language or "", "stars": repo.stargazers_count, "topics": repo.get_topics(), "updated_at": repo.updated_at.isoformat(), }) page += 1 time.sleep(0.5) # 避免触发速率限制 return stars注意:
get_page这个方法在 PyGithub 里是懒加载的,如果你直接对它做切片操作可能会触发额外的请求。建议老老实实用循环,别耍小聪明。
3.2 让 AI 稳定输出结构化分类的提示词设计
这是整个项目里最考验功夫的部分。AI 分类的准确率,八成取决于提示词写得好不好。
我一开始的提示词很随意,大概就是"请给这个项目分类并打标签"。结果 AI 返回的东西五花八门,有时候是纯文本,有时候是 JSON,有时候分类名跟我预想的完全不一样。后来我改成了严格的 JSON 输出格式,并且给了明确的分类体系和示例,效果才稳定下来。
我的提示词结构是这样的:
你是一个开源项目分类助手。请根据以下项目信息,输出一个 JSON 对象。 项目名称:{name} 项目描述:{description} 主要语言:{language} README 摘要:{readme_snippet} 请严格按照以下 JSON 格式输出,不要输出任何其他内容: { "category": "从下面列表中选择一个最合适的分类", "tags": ["标签1", "标签2", "标签3"], "summary": "用一句中文概括这个项目是做什么的,不超过 50 字" } 可选分类列表: - 前端框架 - 后端框架 - 数据库 - DevOps 工具 - AI 与机器学习 - 命令行工具 - 学习资源 - 其他 标签要求:2 到 5 个,用英文,反映项目的技术栈或特点。这里有几个关键点。一是分类列表要固定,不能让 AI 自由发挥,否则它会给你造出"Web 开发相关工具"这种模棱两可的分类。二是要求纯 JSON 输出,方便后续解析。三是给 summary 加字数限制,不然 AI 会写一大段。
即便如此,AI 偶尔还是会不听话,比如在 JSON 外面加一句"好的,以下是结果"。所以解析的时候必须做容错处理,用正则把 JSON 部分抠出来再解析。
3.3 缓存与增量更新机制
前面提到过,AI 调用是有成本的。假设你有 800 个 Star,每个项目调一次 AI,按现在的价格算下来也得几块钱。如果每天都全量跑一遍,一个月就是上百块,完全没必要。
我的做法是用 SQLite 存每个项目的分析结果,key 用full_name,value 存分类、标签、摘要,外加一个updated_at字段记录项目最后更新时间。每次运行时,先拉取最新的 Star 列表,然后逐个对比:如果项目在数据库里不存在,或者项目的updated_at变了,就重新分析;否则直接用缓存。
import sqlite3 def get_cached(conn, full_name, updated_at): cur = conn.execute( "SELECT category, tags, summary FROM projects WHERE full_name=? AND updated_at=?", (full_name, updated_at) ) row = cur.fetchone() if row: return {"category": row[0], "tags": row[1].split(","), "summary": row[2]} return None这样跑下来,第一次全量分析可能要十几分钟,之后每天增量更新通常只有几个新项目,几十秒就搞定了。
实操心得:
updated_at这个字段其实不太准,因为有些项目只是改了个 README 的错别字,updated_at也会变。如果你特别在意成本,可以改成对比 README 内容的 hash,只有内容真的变了才重新分析。我图省事没做这么细,反正增量更新的量不大。
4. 完整实操流程与关键代码实现
4.1 环境准备与依赖安装
先把环境搭起来。我假设你已经装了 Python 3.10 以上版本,并且有一个 GitHub 账号。
第一步,创建一个虚拟环境,别把依赖装到全局去:
python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate第二步,安装依赖:
pip install PyGithub openai requests第三步,准备两个凭证。一个是 GitHub 的 Personal Access Token,在 GitHub 设置里的 Developer settings 里生成,勾选read:user权限即可。另一个是 AI 服务的 API Key,我用的是 OpenAI 兼容接口,你换成任何一家支持这个格式的都行。
把这两个凭证放到环境变量里,别硬编码在代码里:
export GITHUB_TOKEN="ghp_xxxxxxxx" export AI_API_KEY="sk-xxxxxxxx" export AI_BASE_URL="https://api.openai.com/v1" export AI_MODEL="gpt-4o-mini"4.2 主流程代码逐段解析
整个主流程分四步:拉取 Star、对比缓存、调用 AI、输出结果。我把核心代码拆开讲。
先看 AI 调用部分。这里的关键是构造一个稳定的请求,并且处理各种异常情况:
from openai import OpenAI import json import re client = OpenAI( api_key=os.environ["AI_API_KEY"], base_url=os.environ["AI_BASE_URL"], ) def analyze_project(project): prompt = build_prompt(project) try: resp = client.chat.completions.create( model=os.environ["AI_MODEL"], messages=[{"role": "user", "content": prompt}], temperature=0.2, ) text = resp.choices[0].message.content return parse_json_safely(text) except Exception as e: print(f"分析失败 {project['full_name']}: {e}") return None def parse_json_safely(text): # 先尝试直接解析 try: return json.loads(text) except json.JSONDecodeError: pass # 失败则用正则抠出 JSON 部分 match = re.search(r'\{.*\}', text, re.DOTALL) if match: try: return json.loads(match.group()) except json.JSONDecodeError: return None return Nonetemperature设成 0.2 是为了让输出更稳定,别让 AI 太有创造力。分类这种任务不需要创意,需要的是确定性。
再看主循环。这里要注意的是,AI 调用最好加个并发,不然 800 个项目串行跑太慢了。我用concurrent.futures开了 5 个并发,实测下来既不会触发速率限制,速度也快了不少:
from concurrent.futures import ThreadPoolExecutor def process_all(stars, conn): results = [] to_analyze = [] for s in stars: cached = get_cached(conn, s["full_name"], s["updated_at"]) if cached: results.append({**s, **cached}) else: to_analyze.append(s) with ThreadPoolExecutor(max_workers=5) as executor: futures = {executor.submit(analyze_project, p): p for p in to_analyze} for future in futures: project = futures[future] result = future.result() if result: save_to_db(conn, project, result) results.append({**project, **result}) return results4.3 输出成 Markdown 和写回 GitHub List
结果落地我做了两个输出。Markdown 输出很简单,按分类分组,每个项目一行:
def export_markdown(results, path="stars.md"): by_category = {} for r in results: by_category.setdefault(r["category"], []).append(r) with open(path, "w", encoding="utf-8") as f: for cat, items in sorted(by_category.items()): f.write(f"## {cat}\n\n") for item in sorted(items, key=lambda x: -x["stars"]): f.write(f"- [{item['full_name']}](https://github.com/{item['full_name']}) " f"- {item['summary']} `{','.join(item['tags'])}`\n") f.write("\n")写回 GitHub List 稍微麻烦一点。GitHub 的 List 功能目前没有公开的 REST API,只有 GraphQL API 支持。你需要用 GraphQL 的createUserList和updateUserList这两个 mutation。这块代码比较长,我就不全贴了,核心思路是先创建 List,然后批量把项目加进去。
注意:GitHub List 有数量限制,一个账号最多创建 32 个 List,每个 List 最多 1000 个项目。如果你 Star 特别多,可能需要合并一些分类。
4.4 定时任务配置
最后用 cron 配一个每天跑一次的定时任务:
0 3 * * * cd /path/to/project && /path/to/venv/bin/python main.py >> run.log 2>&1凌晨 3 点跑,避开白天用网高峰,也不影响你白天用电脑。日志重定向到文件,方便出问题的时候排查。
5. 常见问题与排查技巧实录
5.1 AI 分类结果不稳定怎么办
这是最常见的问题。同一个项目,今天跑出来是"后端框架",明天跑出来是"DevOps 工具"。原因通常是提示词不够明确,或者模型本身对某些领域理解有偏差。
我的解决办法有三个。一是降低 temperature,从默认的 1.0 降到 0.2,输出会稳定很多。二是在提示词里给示例,比如"一个用 Go 写的 HTTP 框架应该归到'后端框架',而不是'命令行工具'",给几个边界案例,AI 的判断会准很多。三是加一层校验,如果 AI 返回的分类不在预设列表里,就强制归到"其他",并且记录下来,方便后续人工复查。
5.2 GitHub API 速率限制怎么破
即使认证了,每小时 5000 次也不是无限的。如果你 Star 特别多,加上 README 拉取,很容易撞上限。我的经验是:拉取 Star 列表本身消耗不大,800 个项目也就 8 次请求;真正消耗大的是拉 README,每个项目一次,800 个就是 800 次。
解决办法是只对没有缓存的项目拉 README,有缓存的直接跳过。另外,README 拉取可以延迟处理,比如今天拉 200 个,明天拉 200 个,分几天跑完。反正整理收藏夹不是紧急任务,没必要一次性搞定。
5.3 项目描述为空或全是英文怎么办
有些项目描述是空的,有些是纯英文,还有些描述写得极其抽象,比如 "The missing piece of your stack"。这种项目 AI 也很难判断。
我的处理策略是:描述为空时,优先看 README 的前几行;README 也没有时,看项目的 Topics 和主要语言,用这些信息兜底。如果实在判断不出来,就归到"其他",并且在摘要里标注"信息不足,建议人工查看"。这样至少不会误分类。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| AI 返回非 JSON | 提示词不够严格 | 加"只输出 JSON"约束,解析时用正则兜底 |
| 分类结果飘忽 | temperature 太高 | 降到 0.2,并在提示词里给示例 |
| API 报 403 | Token 权限不足或过期 | 重新生成 PAT,确认勾选 read:user |
| 拉取速度慢 | 串行请求 | 用线程池并发,但别超过 5 个 |
| 重复分析同一项目 | 缓存 key 不对 | 用 full_name + updated_at 做联合 key |
| List 写不进去 | GraphQL 权限问题 | 确认 PAT 有userscope |
5.5 几个我踩过的坑
第一个坑是别用repo.get_readme()直接拿内容。这个方法返回的是一个ContentFile对象,内容是 base64 编码的,你得先解码。我一开始直接把它当字符串用,结果 AI 收到的是一堆乱码。
第二个坑是并发数别开太大。我一开始开了 20 个并发,结果 GitHub API 直接给我返回 429,整个任务卡死。后来降到 5 个,稳定运行。
第三个坑是别在提示词里塞太多 README。我一开始把整个 README 都塞进去,结果 token 消耗巨大,而且 AI 反而抓不住重点。后来改成只取前 2000 字符,效果反而更好。
6. 这套方案还能怎么扩展
跑通基础版本之后,我又想了几个可以继续优化的方向,这里也分享一下,给想深入折腾的朋友一点参考。
第一个方向是加一个 Web 界面。现在的结果是 Markdown 文件和 GitHub List,查看还行,但搜索和筛选不方便。可以用 FastAPI 加一个简单的页面,支持按分类、标签、语言筛选,还能全文搜索摘要。这样用起来就顺手多了。
第二个方向是接入项目活跃度判断。现在只做了分类,但收藏夹里其实有很多项目已经停止维护了。可以定期检查项目的最后提交时间,如果超过一年没更新,就在摘要里标注"可能已停止维护",提醒自己别踩坑。
第三个方向是做相似项目去重。收藏夹里经常有功能重复的项目,比如好几个 JSON 解析库。可以让 AI 对比项目描述,找出功能高度重叠的,合并展示,避免选择困难。
第四个方向是支持多账号。如果你有多个 GitHub 账号,或者想帮团队整理共享收藏,可以把 token 和数据库都做成可配置的,一套代码跑多个账号。
这些扩展我自己也只做了一部分,Web 界面还在写,活跃度判断已经加上了。整体来说,这套方案的核心价值不在于技术多复杂,而在于它真的解决了一个日常痛点。以前我找项目靠记忆和搜索,现在打开 Markdown 文件按分类一翻就找到了,效率提升非常明显。
如果你也在被收藏夹混乱困扰,建议先跑一个最小可用版本,把拉取和分类跑通,再慢慢加功能。别一上来就想着做完美,先让它能用,再让它好用。