Skip to content

Commit 032e897

Browse files
mirror29claude
andcommitted
fix(ci): 注入审查规则并预留推理预算
将 CLAUDE.md 直接注入无工具的 Chat Completions prompt,拒绝工具协议泄漏,并扩大 V4 Pro completion budget,确保 reasoning 后仍能输出正文。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 14ce2b8 commit 032e897

2 files changed

Lines changed: 63 additions & 20 deletions

File tree

scripts/deepseek_review.py

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
DIFF_PATH = "/tmp/pr_diff.txt"
2828
TITLE_PATH = "/tmp/pr_title.txt"
29+
RULES_PATH = "CLAUDE.md"
2930
OUT_PATH = "/tmp/review_body.txt"
3031

3132
BASE_URL = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1")
@@ -37,19 +38,20 @@
3738
你是这个仓库的资深 reviewer。目标:在合并前尽量拦住真正的 bug、设计缺陷、架构失误。
3839
**不要只对着固定清单打勾**——清单覆盖不到新功能。要先理解再评审。
3940
40-
## Step 1 · 读项目规则(每次都重新读,规则会随项目迭代而变)
41+
## Step 1 · 读项目规则(每次都重新注入,规则会随项目迭代而变)
4142
42-
- 用 Read 读仓库根 `CLAUDE.md`;改动目录附近若有 `AGENTS.md` / 相关 `docs/` / ADR 也读。
43-
- 把里面的硬约束当成本次 review 的**项目专属规则**——
44-
CLAUDE.md 更新了,你的评审标准就自动跟着更新,**无需改这个 workflow**。
45-
- 这是项目规则的唯一权威来源;下面 Step 4 的清单只是提示,以你读到的为准。
43+
- 用户消息中的“项目规则”就是仓库根 `CLAUDE.md` 的完整内容,把其中硬约束当作
44+
本次 review 的项目专属标准。
45+
- 当前环境没有 Read / Grep / Glob 或其它工具。不要尝试调用工具,也不要输出工具调用协议;
46+
只能基于项目规则、PR 标题和完整 diff 评审。
47+
- 规则更新后会由脚本自动重新注入,无需修改这个 workflow。
4648
4749
## Step 2 · 重建意图 + 圈定影响面
4850
4951
- 先一句话说清这个 PR 想做什么。
50-
- 用 Read / Grep / Glob 看 diff **以外**的代码:改动的函数 / 接口 / 契约有哪些调用方?
51-
碰了哪些模块边界(Inalpha 是 Next.js → Mastra(TS) → Python services 三层)?
52-
- 只有理解了"改动如何与系统其余部分交互",才谈得上架构评审
52+
- 只基于给定完整 diff 重建调用关系和模块边界。看不到 diff 外实现时,不要假装已经读取,
53+
也不要输出工具请求;仅报告能由现有证据支持的问题。
54+
- 只有理解了改动如何与系统其余部分交互,才提出架构 finding
5355
5456
## Step 3 · 通用工程评审(适用任何功能,新增功能也自动覆盖)
5557
@@ -95,16 +97,31 @@ def _fail(msg: str) -> None:
9597
sys.exit(0)
9698

9799

98-
def _call_deepseek(api_key: str, title: str, diff: str) -> str:
99-
"""调用 DeepSeek;正常响应却无正文时重试一次,禁止发布空 review。"""
100+
def _is_valid_review(content: object) -> bool:
101+
"""只接受非空自然语言正文,拒绝模型泄漏的工具调用协议。"""
102+
if not isinstance(content, str) or not content.strip():
103+
return False
104+
protocol_markers = ("<||DSML||tool_calls>", "<tool_call>", '"tool_calls"')
105+
return not any(marker in content for marker in protocol_markers)
106+
107+
108+
def _call_deepseek(api_key: str, title: str, diff: str, rules: str) -> str:
109+
"""调用 DeepSeek;空正文或工具协议泄漏时重试一次。"""
100110
payload = {
101111
"model": MODEL,
102112
"messages": [
103113
{"role": "system", "content": SYSTEM_PROMPT},
104-
{"role": "user", "content": f"## PR 标题\n{title}\n\n## Diff\n{diff}"},
114+
{
115+
"role": "user",
116+
"content": (
117+
f"## 项目规则(CLAUDE.md)\n{rules}\n\n"
118+
f"## PR 标题\n{title}\n\n## Diff\n{diff}"
119+
),
120+
},
105121
],
106122
"temperature": 0.1,
107-
"max_tokens": 4096,
123+
# V4 Pro 会把 reasoning tokens 计入 completion;4096 会在输出正文前耗尽。
124+
"max_tokens": 16384,
108125
}
109126
for attempt in range(MAX_ATTEMPTS):
110127
req = urllib.request.Request(
@@ -119,16 +136,19 @@ def _call_deepseek(api_key: str, title: str, diff: str) -> str:
119136
with urllib.request.urlopen(req, timeout=TIMEOUT_S) as resp:
120137
body = json.loads(resp.read())
121138
content = body["choices"][0]["message"].get("content")
122-
if isinstance(content, str) and content.strip():
139+
if _is_valid_review(content):
123140
return content.strip()
124141
if attempt + 1 < MAX_ATTEMPTS:
125142
payload["messages"].append(
126143
{
127144
"role": "user",
128-
"content": "上一轮没有返回 review 正文。请直接输出最终中文 review。",
145+
"content": (
146+
"上一轮没有返回可发布的 review 正文。当前没有任何工具,"
147+
"禁止输出工具调用协议。请直接输出最终中文 review。"
148+
),
129149
}
130150
)
131-
raise ValueError("DeepSeek 返回空 review 正文")
151+
raise ValueError("DeepSeek 未返回可发布的 review 正文")
132152

133153

134154
def main() -> None:
@@ -141,14 +161,16 @@ def main() -> None:
141161
diff = f.read()
142162
with open(TITLE_PATH) as f:
143163
title = f.read().strip()
164+
with open(RULES_PATH) as f:
165+
rules = f.read()
144166
except OSError as e:
145167
_fail(f"读输入失败:{e}")
146168

147169
if not diff.strip():
148170
_fail("diff 为空")
149171

150172
try:
151-
content = _call_deepseek(api_key, title, diff)
173+
content = _call_deepseek(api_key, title, diff, rules)
152174
except urllib.error.HTTPError as e:
153175
body = e.read().decode("utf-8", "replace")[:300]
154176
_fail(f"DeepSeek API HTTP {e.code}{body}")

tests/test_deepseek_review.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,16 @@ def fake_urlopen(request, *, timeout):
5656
return _FakeResponse()
5757

5858
with patch.object(module.urllib.request, "urlopen", side_effect=fake_urlopen):
59-
result = module._call_deepseek("secret-value", "PR title", "diff body")
59+
result = module._call_deepseek("secret-value", "PR title", "diff body", "project rules")
6060

6161
request = captured["request"]
6262
payload = json.loads(request.data)
6363
self.assertEqual(result, "LGTM")
6464
self.assertEqual(request.full_url, "https://api.deepseek.com/v1/chat/completions")
6565
self.assertEqual(request.get_header("Authorization"), "Bearer secret-value")
6666
self.assertEqual(payload["model"], "deepseek-v4-pro")
67+
self.assertEqual(payload["max_tokens"], 16384)
68+
self.assertIn("## 项目规则(CLAUDE.md)\nproject rules", payload["messages"][1]["content"])
6769
self.assertEqual(captured["timeout"], module.TIMEOUT_S)
6870

6971
def test_empty_content_retries_then_returns_review(self) -> None:
@@ -75,7 +77,24 @@ def test_empty_content_retries_then_returns_review(self) -> None:
7577
"urlopen",
7678
side_effect=responses,
7779
) as urlopen:
78-
result = module._call_deepseek("secret-value", "PR title", "diff body")
80+
result = module._call_deepseek("secret-value", "PR title", "diff body", "project rules")
81+
82+
self.assertEqual(result, "最终 review")
83+
self.assertEqual(urlopen.call_count, 2)
84+
85+
def test_tool_protocol_retries_then_returns_review(self) -> None:
86+
module = _load_module()
87+
protocol = '<||DSML||tool_calls><||DSML||invoke name="Read">'
88+
responses = [_FakeResponse(protocol), _FakeResponse("最终 review")]
89+
90+
with patch.object(
91+
module.urllib.request,
92+
"urlopen",
93+
side_effect=responses,
94+
) as urlopen:
95+
result = module._call_deepseek(
96+
"secret-value", "PR title", "diff body", "project rules"
97+
)
7998

8099
self.assertEqual(result, "最终 review")
81100
self.assertEqual(urlopen.call_count, 2)
@@ -88,8 +107,10 @@ def test_repeated_empty_content_fails_instead_of_publishing_blank_review(self) -
88107
"urlopen",
89108
side_effect=[_FakeResponse(""), _FakeResponse(" ")],
90109
):
91-
with self.assertRaisesRegex(ValueError, "空 review 正文"):
92-
module._call_deepseek("secret-value", "PR title", "diff body")
110+
with self.assertRaisesRegex(ValueError, "未返回可发布"):
111+
module._call_deepseek(
112+
"secret-value", "PR title", "diff body", "project rules"
113+
)
93114

94115
def test_missing_key_writes_non_blocking_failure(self) -> None:
95116
module = _load_module()

0 commit comments

Comments
 (0)