PR 被拒三次之后:给 Hindsight 提代码,开源记忆项目的真实合入规则
PR 被拒三次之后:给 Hindsight 提代码,开源记忆项目的真实合入规则
在开源生态里,"PR 被拒三次"几乎是每个贡献者的成人礼:第一次因为没跑测试,第二次因为提交信息太糊,第三次因为改动范围失控。但对 Hindsight——这个由 Vectorize.io 开源的 AI Agent 长期记忆系统、近期因 retain / recall / reflect 三层记忆架构在 LongMemEval 等基准上登顶而频繁登上趋势榜的项目——事情的开局远比"被拒三次"更残酷:外部贡献者的 PR 根本不会被 review,而是直接关闭。这条规则写在仓库根目录的 CONTRIBUTING.md 第一段,白纸黑字:
We don't accept pull requests from outside the Hindsight team. PRs opened by external contributors are closed without review.
换句话说,如果你以路人身份 fork 一份代码、埋头改完三天后自信地提 PR,你连"被拒"这个流程都走不到。这篇文章不想贩卖鸡汤,而是基于仓库里的真实源码与文档,把 Hindsight 的"真实合入门"拆开:为什么它敢拒绝外部 PR、团队内部的合入要过哪几道闸、以及一个真心想贡献的人应该把力气花在哪里。
一、先读懂合入门:不是傲慢,是"issue 优先"的治理模型
很多人把"不接受外部 PR"误读成项目封闭。细读 CONTRIBUTING.md 会发现,它给出的是另一套完整的贡献路径:
- Bug:提交 bug report,最有价值的是可复现路径("A clear way to reproduce it is the most useful thing you can give us");
- 功能 / 新集成 / 扩展 / bank 模板:提交 feature request;
- 疑问与想法:走 GitHub Discussions;
- 安全问题:按 SECURITY.md 走非公开通道。
文件里还给了官方理由:"it lets the team decide what gets built, review fewer half-finished changes, and ship fixes together"——让团队决定做什么、少 review 半成品、把修复合并发布。这其实是一种相当常见的开源治理策略:用 issue 对话代替 PR 碰撞,把评审资源留给团队内部的定向开发。社区里流传的"给 Hindsight 提 PR 被 maintainer 打回三轮修改意见"的叙事,严格来说只适用于两条路:一是你已经是团队认可的外部协作者,二是你先通过 issue 把方案聊透了,被邀请进来改。
所以第一条真实规则是:不要一上来就写代码。先去 issues 里找"无人认领"的 bug 或 feature request,用可复现步骤和预期行为把问题讲清楚——这正是 CONTRIBUTING.md 反复强调的贡献起点。你的第一份"代码"应该是文字。
二、pre-commit 钩子与测试先行的硬规矩
假设你确实进入了可以提代码的阶段(团队内部成员或受邀协作者),等待你的是一套从"本地环境"到"commit 瞬间"都被机器接管的质量闸门。
一条命令搭出可复现环境
scripts/dev/setup.sh 把整个开发环境的初始化做成了幂等脚本:装齐 uv / Node / Rust 工具链、从 .env.example 生成 .env、配置 git hooks、安装 Python 与 Node workspace 依赖、预下载本地 embedding/reranker 模型,并构建 TypeScript SDK 与 Rust CLI。脚本注释里写得很直白——"ready to develop with — including offline"。这套"环境可复现"是后续一切讨论的前提:reviewer 不会要求你本地跑 Docker,但会假设你的测试结果可以被任何人复现。
钩子替你把关:.githooks/pre-commit 的真实逻辑
git clone 后运行 scripts/setup-hooks.sh,它执行一条命令把 core.hooksPath 指向 .githooks:
git config core.hooksPath "$REPO_ROOT/.githooks"
真正的检查逻辑在 .githooks/pre-commit 中,它遍历执行 scripts/hooks/ 下所有可执行脚本——目前是 lint.sh 与 check-unused.sh。
scripts/hooks/lint.sh 的注释第一行就是"Parallel linting for all code",它在 commit 时并行跑完全部检查:
- Python(hindsight-api-slim / hindsight-dev / hindsight-embed 三份):
ruff check --fix、ruff format、ty check(Astral 的极速类型检查器); - TypeScript:控制面板的
eslint --fix、prettier --write,外加两个 SDK 包的 prettier; - 集成包:只 lint 相对 HEAD 有改动的包(CI 里则全量),Python 集成统一走仓库根 ruff.toml,Node 集成走共享的
.prettierrc.json。
脚本开头的 uv sync --frozen 很有讲究——先同步依赖避免并行 uv run 的竞争,同时用 --frozen 防止钩子擅自改写 uv.lock:"Dependency changes should update the lockfile explicitly before linting." 改依赖必须先显式更新锁文件,钩子不帮你擦屁股。
scripts/hooks/check-unused.sh 则把死代码检查分成两档:blocking 档由 lint.sh 的 ruff(未使用的 import F401 / 变量 F841)与 CI 的 verify-generated-files、knip 强制拦截;advisory 档用 vulture 扫描整个未使用的函数/类(--min-confidence 80),明知 FastAPI/Pydantic 会误报,所以"always exits 0",定位为 review 辅助而非门槛。
测试先行:633 个测试文件撑起来的确定性
CLAUDE.md 给出了测试铁律,而 hindsight-api-slim/tests/ 目录下如今躺着 633 个测试文件。几个关键约定:
- 确定性逻辑直接断言:绝大多数测试用 MockLLM 与纯函数,跑得快且可复现;
- LLM 行为必须用 LLM 当裁判:凡是"模型如何理解 prompt"(
fact_type分类、说话人归属、指令遵循),MockLLM 无法模拟,必须打pytest.mark.hs_llm_core标记、走真实 pipeline,并用 llm_judge.py 的assert_meets_criteria断言,而不是硬编码字符串; - 用户可见的能力必须配黑盒系统测试:改动如果新增了"用户能叫出名字"的能力、或让两个既有能力首次相遇,就得在 hindsight-system-tests/ 里加一个走公开 Python client 的 story——它禁止引擎内访问、禁止 SQL,专抓单测结构上抓不到的"步骤拼接处"的 bug(consolidation 清掉了自己脚下的 facts、transfer 弄丢了证据这一类)。
.claude/skills/code-review/SKILL.md 甚至把"新 API 端点必须有集成测试、新工具函数必须有单测、bug 修复必须有回归测试"写成 must-fix 级审查项。社区里那句"一个没有测试的 bug 修复大概率被退回,带复现测试的修复往往一次过"——说的就是这套机制。
三、PR 粒度单一与"3 句话描述"的合入潜规则
如果测试全绿、钩子全过,你终于来到写 PR 这一步。仓库里没有专门的 PR template 文件(find_files 未找到 .github/ 下的模板),但合入的"潜规则"散落在代码评审标准与维护者约定里。
分支卫生是硬约束
SKILL.md 的 Branch Hygiene 一节写得毫不含糊:
- 新功能分支必须从
origin/main切出,rebase 保持基底干净; - 分支里只允许与本 PR 相关的提交,混入无关改动必须移除后才能合入;
- review 的第一步就是
git log --oneline main..HEAD逐条核对提交相关性。
这直接引出"一个 PR 只讲一件事"的粒度规则。对应地,CLAUDE.md 里有一条容易被踩的坑:开发期间 bump 版本号不需要重新生成客户端,客户端只在 scripts/release.sh 正式发布时由 scripts/generate-clients.sh 统一再生成——"Do not manually run generate-clients.sh unless testing generation changes"。把"改版本号 + 顺手重生成客户端"塞进一个功能 PR,就是典型的无关改动。
"3 句话描述":动机、做法、验证
社区流传的 Hindsight 贡献手册总结的 PR 描述规范,与仓库维护者的实际关注点完全吻合,概括为三句话就够:
- 动机:修哪个 issue、加什么行为;
- 做法:改了哪几个模块、为什么这么改;
- 验证:跑了哪些测试、结果如何,并附上 issue 编号。
这套"三句结构"不是形式主义,而是给 reviewer 的导航:他们最在意的永远是 diff 的粒度,测试文件里混进无关的格式重排,会在 review 里被单独拎出来。
代码风格上的隐性红线
即便过了 lint,SKILL.md 还列着一批"人类 reviewer 才会抓"的红线,值得任何想合入的人提前记住:
- 禁止多元素 tuple 返回值("No exceptions, no 'it's just two values' shortcuts")——一律用 dataclass 或 Pydantic 模型;
- 禁止裸
dict承载结构化数据——已知键就必须建模,dict.get()模式属于违规; - Handler 层禁止直连数据库——HTTP 路由里出现
conn.fetch/ 原始 SQL /fq_table(...)是 must-fix,查询必须下沉到MemoryEngine方法并返回类型化模型; - bank 隔离是不可妥协的安全不变量——任何触碰多 bank 表(
memory_units、documents、entities……)的 SQL 都必须带bank_id谓词或满足三种合法作用域之一,否则就是跨 bank 读写泄漏(SKILL.md 甚至给了 #3429/#3430 的真实事故作为注脚); - 生产代码禁用同步 HTTP 与
httpx——一律 aiohttp + async,asyncio.to_thread包一层不算是修复; - 禁用 PostgreSQL advisory lock,禁用
BaseHTTPMiddleware(移除两个中间件后/health/live从约 2.4k 涨到约 7.9k rps、p99 从约 107ms 降到约 18ms 的数据被写进了文档)。
这些不是风格偏好,而是踩过生产事故后沉淀下来的规则。合入门槛高,正是因为这套系统跑在真实的多租户、多事件循环环境里。
四、被 maintainer 打回修改意见后的高效响应策略
即便一切合规,内部合入流程依然会有 change request——对团队而言这同样是常态。社区手册和仓库文档共同指向一套可操作的响应打法:
把 change request 当清单,不当判决书
处理顺序是:从上到下逐条读意见 → 能改的直接改 → 不能认同的在对应评论下回复理由 → 改完对同一分支 force-push,reviewer 会自动收到通知。这里有一条维护者视角的忠告:最让人反感的不是改得慢,而是"沉默地 force-push"——每轮修改后在 PR 里逐条回复"已改 / 说明理由",下一轮 review 的速度会明显变快。
三个常见卡点与对应解法
- CI 红、本地绿:先对比 CI 与本地 Node/Python 版本。仓库根 ruff.toml 统一了 Python 检查配置(line-length 120、target py311),本地用别的配置跑出不同结果是典型原因;
- reviewer 要求拆 PR:按建议拆成两个分支,小的先合、大的后提,不要硬扛;
- 改着改着发现方案错了:用
git revert把整个分支撤掉重新设计,比在坏方案上叠补丁快得多——这正是"分支里只留相关提交"的价值,撤起来干净。
两轮之后:把分歧摆上台面
社区手册给出的经验阈值是:被拒两次以上的 PR,先在 issue 区或 Discussion 里同步进展,把方案分歧摆到台面上讨论,而不是继续在 PR 里往返拉扯。这与 CONTRIBUTING.md 的"Questions and ideas: use GitHub Discussions"治理结构是一致的——方案层的争议属于公共对话,不属于代码 diff。
结语:Hindsight 的真实合入规则是什么
回到标题:如果你的"PR 被拒三次"发生在 Hindsight,大概率第一个回合就被关闭了——因为外部 PR 不进入 review 流程。这不是坏消息,反而帮你省下了三次无效劳动。真正的合入规则可以浓缩成四句话:
- 先通过 issue 赢得"被邀请"的资格——可复现的 bug 报告、讲清收益的 feature request,才是这个项目认可的贡献货币;
- 环境与测试是入场券——
./scripts/dev/setup.sh一键复现环境,633 个测试文件、pre-commit 并行 lint 是每个 commit 的必经之路; - 粒度与叙事是通行证——一个 PR 一件事,描述按"动机 / 做法 / 验证"三句话写,分支只留相关提交;
- 响应节奏决定存活率——change request 逐条回应、两轮后升级到 Discussion,比任何代码技巧都更能推动合入。
这套规则的内核,其实藏在 SKILL.md 的一句审查标准里:"Don't add features, refactor code, or make 'improvements' beyond what was asked." 一个靠记忆系统让 Agent"越用越聪明"的项目,对代码合入的苛刻,本质上是对系统长期可演进性的保护——它不接受半成品,不是因为傲慢,而是因为每一个合入的 commit 都会被 633 个测试、三套语言工具链和上百条安全红线反复校验。看懂这层逻辑,你就知道下一次该把力气花在哪里了。