拥抱AI-AI辅助测试与文档 —— 专业度的分水岭
本章导读:代码写完了,SQL也跑通了,但你能放心上线吗?没有测试的代码就像没有安全带的赛车——迟早出事。没有文档的代码就像没有地图的宝藏——只有你能找到,但也只有现在。本章教你用AI自动化测试生成和文档编写,让你从”写完代码”进化到”交付可靠产品”。
7.1 为什么测试和文档是你的护城河
一个真实对比
没有测试和文档的张三:
-
写代码:1天
-
上线后:凌晨3点被叫醒(数据算错了)
-
排查:2小时找到bug(边界条件没处理)
-
修复:30分钟
-
被老板问:”为什么这么简单的问题都测不出来?”
-
一个月后:自己也看不懂自己写的代码了
有AI辅助测试和文档的李四:
-
写代码:1天
-
AI生成测试:30分钟
-
测试发现边界bug:5分钟修复
-
上线:平稳运行
-
AI生成文档:15分钟
-
一个月后:看文档10分钟回忆起来
差距:不仅仅是时间,更是职业声誉。
7.2 AI辅助单元测试
7.2.1 为ProcessFunction生成测试
提示词模板:
为以下Flink ProcessFunction生成单元测试:[粘贴ProcessFunction代码]测试要求:1. 使用FlinkMiniCluster或KeyedOneInputStreamOperatorTestHarness2. 测试场景:- 正常输入:单个元素,验证输出正确- 多元素:验证状态累加正确- 水位线推进:验证定时器触发正确- 迟到数据:验证侧输出- 状态恢复:模拟checkpoint和重启3. 断言:验证输出数据的字段值4. 使用JUnit 5生成完整的测试类代码。
package com.company.realtime.profile.process;import org.apache.flink.api.common.state.ValueStateDescriptor;import org.apache.flink.api.common.typeinfo.TypeInformation;import org.apache.flink.streaming.api.operators.KeyedProcessOperator;import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;import org.apache.flink.streaming.util.KeyedOneInputStreamOperatorTestHarness;import org.apache.flink.streaming.util.TestHarnessUtil;import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.Test;import java.util.concurrent.ConcurrentLinkedQueue;import static org.junit.jupiter.api.Assertions.*;class LongTermProfileFunctionTest {private KeyedOneInputStreamOperatorTestHarness<String, UserAction, UserProfile> testHarness;private LongTermProfileFunction processFunction;@BeforeEachvoid setUp() throws Exception {processFunction = new LongTermProfileFunction();testHarness = new KeyedOneInputStreamOperatorTestHarness<>(new KeyedProcessOperator<>(processFunction),UserAction::getUserId,TypeInformation.of(String.class));testHarness.open();}@Testvoid testNormalProcessing() throws Exception {// 准备测试数据UserAction action1 = new UserAction("u1", ActionType.CLICK, "p1", "electronics", 0, 1000L);UserAction action2 = new UserAction("u1", ActionType.PURCHASE, "p1", "electronics", 99.9, 2000L);// 注入数据testHarness.processElement(new StreamRecord<>(action1, 1000L));testHarness.processElement(new StreamRecord<>(action2, 2000L));// 模拟时间推进(触发定时器)testHarness.setProcessingTime(61000L); // 1分钟后// 收集输出ConcurrentLinkedQueue<Object> output = testHarness.getOutput();// 验证assertFalse(output.isEmpty());StreamRecord<UserProfile> record = (StreamRecord<UserProfile>) output.poll();UserProfile profile = record.getValue();assertEquals("u1", profile.getUserId());assertEquals(1, profile.getClickCount());assertEquals(99.9, profile.getPurchaseAmount());assertEquals(UserProfile.WindowType.LONG, profile.getWindowType());}@Testvoid testStateRecoveryAfterCheckpoint() throws Exception {// 1. 处理第一条数据UserAction action = new UserAction("u1", ActionType.CLICK, "p1", "electronics", 0, 1000L);testHarness.processElement(new StreamRecord<>(action, 1000L));// 2. 做checkpointtestHarness.snapshot(1L, 1000L);// 3. 关闭并恢复testHarness.close();testHarness.initializeState(1L);testHarness.open();// 4. 处理第二条数据UserAction action2 = new UserAction("u1", ActionType.PURCHASE, "p1", "electronics", 99.9, 2000L);testHarness.processElement(new StreamRecord<>(action2, 2000L));// 5. 触发定时器testHarness.setProcessingTime(61000L);// 6. 验证状态恢复了(累计了第一条数据)ConcurrentLinkedQueue<Object> output = testHarness.getOutput();StreamRecord<UserProfile> record = (StreamRecord<UserProfile>) output.poll();assertEquals(1, record.getValue().getClickCount());assertEquals(99.9, record.getValue().getPurchaseAmount());}@Testvoid testLateDataHandling() throws Exception {// 先处理一个正常数据UserAction normal = new UserAction("u1", ActionType.CLICK, "p1", "electronics", 0, 1000L);testHarness.processElement(new StreamRecord<>(normal, 1000L));// 推进水位线,关闭窗口testHarness.setProcessingTime(70000L);// 发送迟到数据(定时器已经触发)UserAction late = new UserAction("u1", ActionType.PURCHASE, "p1", "electronics", 99.9, 2000L);testHarness.processElement(new StreamRecord<>(late, 2000L));// 验证迟到数据被丢弃或进入侧输出// 根据业务逻辑,可能丢弃,不验证具体行为,只验证不崩溃assertNotNull(testHarness.getOutput());}}
7.2.2 为Flink SQL生成测试
提示词模板:
为以下Flink SQL查询生成测试用例:SQL:[粘贴SQL]测试框架:Flink SqlValidator或FlinkMiniCluster测试数据(内联):- source表数据:手动构造List<Row>- 期望结果:预期输出测试场景:1. 正常场景:验证聚合正确2. 边界场景:空输入,验证不输出3. 乱序场景:带水位线的测试要求:- 使用TableEnvironment- 使用registerDataStream或createTemporaryView- 使用executeSql执行查询- 使用collect()收集结果并断言生成完整的JUnit测试类。
package com.company.realtime.sql;import org.apache.flink.table.api.TableEnvironment;import org.apache.flink.table.api.TableResult;import org.apache.flink.types.Row;import org.junit.jupiter.api.Test;import java.util.List;import static org.junit.jupiter.api.Assertions.*;class ClickStatsSqlTest {@Testvoid testRollingWindowAggregation() throws Exception {// 1. 创建TableEnvironmentTableEnvironment env = TableEnvironment.create(EnvironmentSettings.newInstance().inStreamingMode().build());// 2. 注册源表(用VALUES模拟数据)env.executeSql("CREATE TABLE click_log (" +" user_id STRING," +" product_id STRING," +" event_time TIMESTAMP(3)," +" WATERMARK FOR event_time AS event_time - INTERVAL '5' SECOND" +") WITH (" +" 'connector' = 'values'," +" 'data-id' = 'test-data'" +")");// 3. 插入测试数据(Flink 1.15+支持)env.executeSql("INSERT INTO click_log VALUES " +"('u1', 'p1', TIMESTAMP '2024-01-15 10:00:00')," +"('u1', 'p1', TIMESTAMP '2024-01-15 10:00:10')," +"('u2', 'p1', TIMESTAMP '2024-01-15 10:00:20')," +"('u1', 'p2', TIMESTAMP '2024-01-15 10:00:30')");// 4. 执行查询TableResult result = env.executeSql("SELECT " +" product_id," +" TUMBLE_START(event_time, INTERVAL '1' MINUTE) AS window_start," +" COUNT(*) AS click_count," +" COUNT(DISTINCT user_id) AS uv_count " +"FROM click_log " +"GROUP BY product_id, TUMBLE(event_time, INTERVAL '1' MINUTE)");// 5. 收集结果List<Row> rows = result.collect().stream().toList();// 6. 断言assertEquals(2, rows.size()); // p1和p2各一个窗口// 验证p1的统计Row p1Row = rows.stream().filter(r -> r.getField(0).equals("p1")).findFirst().orElseThrow();assertEquals(3L, p1Row.getField(2)); // click_count=3assertEquals(2L, p1Row.getField(3)); // uv_count=2 (u1,u2)// 验证p2的统计Row p2Row = rows.stream().filter(r -> r.getField(0).equals("p2")).findFirst().orElseThrow();assertEquals(1L, p2Row.getField(2)); // click_count=1assertEquals(1L, p2Row.getField(3)); // uv_count=1}}
7.3 AI辅助数据质量验证
7.3.1 生成数据质量检查SQL
提示词模板:
为实时数据管道生成数据质量检查SQL:数据流:user_behavior_log(user_id, action_type, item_id, event_time)检查项:1. 完整性:user_id不为null2. 有效性:action_type在(click, purchase, favorite)内3. 时效性:event_time不晚于当前时间4. 一致性:item_id不为空且长度>05. 唯一性:同一window内相同user_id+item_id去重(可选)要求:- 生成Flink SQL查询,输出质量报告- 报告字段:check_name, total_count, invalid_count, invalid_rate- 使用侧输出流收集异常数据生成SQL。
-- 1. 完整性检查:user_id为nullSELECT'null_user_id' AS check_name,COUNT(*) AS total_count,SUM(CASE WHEN user_id IS NULL THEN 1 ELSE 0 END) AS invalid_count,CAST(SUM(CASE WHEN user_id IS NULL THEN 1 ELSE 0 END) AS DOUBLE) / COUNT(*) AS invalid_rateFROM user_behavior_logUNION ALL-- 2. 有效性检查:action_type不在枚举中SELECT'invalid_action_type' AS check_name,COUNT(*) AS total_count,SUM(CASE WHEN action_type NOT IN ('click', 'purchase', 'favorite') THEN 1 ELSE 0 END) AS invalid_count,CAST(SUM(CASE WHEN action_type NOT IN ('click', 'purchase', 'favorite') THEN 1 ELSE 0 END) AS DOUBLE) / COUNT(*) AS invalid_rateFROM user_behavior_logUNION ALL-- 3. 时效性检查:event_time是未来时间SELECT'future_event_time' AS check_name,COUNT(*) AS total_count,SUM(CASE WHEN event_time > CURRENT_TIMESTAMP THEN 1 ELSE 0 END) AS invalid_count,CAST(SUM(CASE WHEN event_time > CURRENT_TIMESTAMP THEN 1 ELSE 0 END) AS DOUBLE) / COUNT(*) AS invalid_rateFROM user_behavior_logUNION ALL-- 4. 一致性检查:item_id为空SELECT'empty_item_id' AS check_name,COUNT(*) AS total_count,SUM(CASE WHEN item_id IS NULL OR CHAR_LENGTH(item_id) = 0 THEN 1 ELSE 0 END) AS invalid_count,CAST(SUM(CASE WHEN item_id IS NULL OR CHAR_LENGTH(item_id) = 0 THEN 1 ELSE 0 END) AS DOUBLE) / COUNT(*) AS invalid_rateFROM user_behavior_log;
7.3.2 生成异常数据侧输出
提示词:
在Flink DataStream API中,实现数据质量过滤和侧输出:要求:1. 完整性检查:user_id为null → 侧输出2. 有效性检查:action_type不在枚举内 → 侧输出3. 时效性检查:event_time > 当前时间 → 侧输出(告警但保留数据)4. 正常数据:主输出定义3个侧输出流:- invalidNullSideOutput- invalidActionSideOutput- futureEventSideOutput生成完整的ProcessFunction代码。
7.4 AI辅助性能基准测试
7.4.1 生成压力测试脚本
提示词模板:
为Flink作业生成压力测试脚本:作业:UserProfileJob(用户画像)集群:4个TaskManager,每个4核8G测试场景:1. 正常负载:10k条/秒2. 峰值负载:50k条/秒(持续5分钟)3. 反压测试:100k条/秒(持续1分钟,观察反压恢复)测试指标:- 端到端延迟(P50, P99, P999)- 吞吐量(records/s)- Checkpoint耗时和大小- 状态大小增长趋势- 资源使用(CPU/内存)要求:- 使用Apache JMeter或自定义Kafka生产者- 编写脚本:启动Flink作业,发送测试数据,收集metrics- 输出报告模板生成Shell脚本或Python脚本。
# flink_performance_test.pyimport timeimport jsonimport randomfrom kafka import KafkaProducerfrom prometheus_api_client import PrometheusConnectimport subprocessclass FlinkPerformanceTest:def __init__(self):self.producer = KafkaProducer(bootstrap_servers=['localhost:9092'],value_serializer=lambda v: json.dumps(v).encode('utf-8'))self.prometheus = PrometheusConnect(url="http://localhost:9090")def test_normal_load(self):"""测试正常负载:10k/s"""print("开始正常负载测试...")start_time = time.time()total_sent = 0target_rate = 10000 # 10k/swhile time.time() - start_time < 300: # 5分钟batch_start = time.time()sent_in_this_batch = 0# 每秒发送目标数量while time.time() - batch_start < 1 and sent_in_this_batch < target_rate:self.send_random_event()sent_in_this_batch += 1total_sent += 1# 等待到这一秒结束time.sleep(max(0, 1 - (time.time() - batch_start)))duration = time.time() - start_timeactual_rate = total_sent / durationprint(f"正常负载测试完成: 实际速率={actual_rate:.0f}/s")# 收集metricsself.collect_metrics()def test_peak_load(self):"""测试峰值负载:50k/s,持续5分钟"""print("开始峰值负载测试...")# 实现类似逻辑,target_rate=50000def test_backpressure_recovery(self):"""测试反压恢复"""print("开始反压恢复测试...")# 1. 先发送正常负载5分钟self.send_load(10000, 300)# 2. 突然增加到100k/s,持续1分钟self.send_load(100000, 60)# 3. 观察恢复时间recovery_start = time.time()self.send_load(10000, 300)recovery_time = time.time() - recovery_startprint(f"反压恢复时间: {recovery_time:.1f}秒")def collect_metrics(self):"""从Flink REST API收集指标"""# 调用Flink REST APIresult = subprocess.run(['curl', '-s', 'http://localhost:8081/jobs'],capture_output=True, text=True)# 解析并记录...def generate_report(self):"""生成测试报告"""report = f"""===== Flink性能测试报告 =====正常负载 (10k/s):- 吞吐量: {self.normal_load_throughput:.0f} records/s- 延迟 P99: {self.normal_load_latency_p99:.0f} ms- Checkpoint大小: {self.normal_load_checkpoint_size:.2f} MB峰值负载 (50k/s):- 是否反压: {self.peak_load_backpressure}- 延迟增加: {self.peak_load_latency_increase}x反压恢复测试:- 恢复时间: {self.recovery_time:.1f}秒建议:{self.get_recommendations()}"""print(report)if __name__ == "__main__":tester = FlinkPerformanceTest()tester.test_normal_load()tester.test_peak_load()tester.test_backpressure_recovery()tester.generate_report()
7.5 AI辅助文档生成
7.5.1 自动生成运维手册
提示词模板:
为以下Flink作业生成运维手册:[粘贴完整的Flink作业代码]要求:1. 作业概述:做什么、为什么2. 启动方式:命令行、参数说明3. 配置参数:所有可配置项及其含义4. 监控指标:关键指标及正常范围5. 常见问题:已知问题和解决方案6. 故障排查:日志关键字、排查步骤7. 扩缩容指南:如何调整并行度8. 止损方案:出现问题时的紧急操作输出格式:Markdown
# UserProfileJob 运维手册## 1. 作业概述**功能**:实时计算用户画像标签(短期5分钟滑动窗口 + 长期1小时定时器)**数据流**:Kafka(user-actions) → Flink → Redis(user:profile:*)**负责人**:实时数据团队## 2. 启动方式### 2.1 命令行启动```bash./bin/flink run \-c com.company.realtime.profile.UserProfileJob \-p 16 \-yD taskmanager.memory.process.size=4096m \-yD state.backend.rocksdb.incremental=true \./realtime-user-profile-1.0.0.jar \--kafka.brokers kafka01:9092,kafka02:9092 \--redis.host redis-cluster.prod
2.2 参数说明
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| –kafka.brokers | String | 是 | – | Kafka broker地址 |
| —redis.host | String | 是 | – | Redis主机 |
| –redis.port | Int | 否 | 6379 | Redis端口 |
3. 监控指标
3.1 关键指标
| 指标 | 正常范围 | 告警阈值 | 说明 |
|---|---|---|---|
| 端到端延迟 | < 3秒 | > 5秒 | 从Kafka到Redis的时间 |
| Checkpoint耗时 | < 30秒 | > 60秒 | 超过2分钟可能导致失败 |
| 状态大小 | < 2GB/subtask | > 4GB | 过大导致GC和恢复慢 |
| 反压比例 | 0% | > 50% | 高于50%需要扩容 |
3.2 查看指标
# Flink Web UIhttp://flink-ui:8081# 通过REST APIcurl http://flink-ui:8081/jobs/{jobid}/metrics?get=numRecordsInPerSecond
4. 常见问题
4.1 问题1:延迟突然飙升
症状:端到端延迟从2秒涨到10秒
可能原因:
-
Redis写入变慢
-
Kafka分区rebalance
-
GC暂停
排查步骤:
-
查看Flink Web UI反压指标
-
查看TaskManager GC日志
-
查看Redis慢查询日志
解决方案:
-
增加并行度:
-p 32 -
增加内存:
taskmanager.memory.process.size=8g -
临时降级:关闭部分标签计算
4.2 问题2:状态恢复慢
症状:作业重启后,恢复状态耗时>30分钟
解决方案:
-
开启增量checkpoint
-
减少状态TTL
-
升级RocksDB版本
5. 扩缩容指南
5.1 增加并行度
# 1. 从savepoint停止./bin/flink stop --savepointPath hdfs://savepoint $JOB_ID# 2. 以新并行度启动./bin/flink run -s hdfs://savepoint/savepoint-xxx -p 32 ...
5.2 注意事项
-
并行度只能增加,不能减少(否则状态无法恢复)
-
建议在低峰期操作
6. 止损方案
6.1 紧急停止
# 取消作业(不保存savepoint)./bin/flink cancel $JOB_ID# 或者带savepoint停止./bin/flink stop --savepointPath hdfs://emergency $JOB_ID
6.2 降级策略
临时修改配置,关闭部分计算:
-
禁用类目偏好计算(最耗状态)
-
长期画像从1分钟改为5分钟输出
7. 联系方式
-
开发负责人:张三 @zhangsan
-
On-call值班:实时数据团队群
### 7.5.2 生成架构图(Mermaid格式)**提示词**:
为UserProfileJob生成架构图,使用Mermaid格式:
组件:
-
Kafka Source (user-actions)
-
Flink Job (UserProfileJob)
-
Deserialize & Watermark
-
KeyBy userId
-
ShortTermProfile (5分钟滑动窗口)
-
LongTermProfile (1小时定时器)
-
Union
-
Redis Sink
-
Redis (user:profile:*)
要求:
-
数据流方向
-
标注关键配置(并行度、窗口大小)
-
标注侧输出流(异常数据)
生成Mermaid代码。
**Claude生成的Mermaid**:```mermaidgraph TDA[Kafka: user-actions<br/>16 partitions] -->|JSON| B[Deserialize & Watermark<br/>并行度16]B -->|WatermarkStrategy: 5s乱序| C[KeyBy user_id]C -->|Split| D[ShortTerm Profile]C -->|Split| E[LongTerm Profile]subgraph D [短期画像 - 滑动窗口]D1[窗口: 5分钟长度<br/>10秒步长]D2[AggregateFunction<br/>增量聚合]D3[WindowFunction<br/>输出画像]D1 --> D2 --> D3endsubgraph E [长期画像 - 定时器]E1[ValueState累加器<br/>TTL=7天]E2[定时器: 1分钟触发]E3[输出累计画像]E1 --> E2 --> E3endD3 --> F[Union]E3 --> FF -->|写入Redis| G[Redis: user:profile:*<br/>TTL: 短期5分钟/长期1小时]B -.->|侧输出| H[Dead Letter Queue<br/>JSON解析失败]style D fill:#f9f,stroke:#333style E fill:#bbf,stroke:#333style G fill:#bfb,stroke:#333
7.5.3 生成API文档
提示词:
为UserProfileJob生成API文档:输出格式:Markdown,包含:1. 输入数据格式(Kafka消息Schema)2. 输出数据格式(Redis中的JSON)3. 错误码和异常4. 配置项说明要求:- 字段类型、说明、示例值- 必填/可选标识- 版本变更记录
7.6 知识沉淀:让AI学习团队规范
7.6.1 创建团队Prompt库
目录结构:
.claude/├── prompts/│ ├── generate-flink-job.yaml # 生成Flink作业的模板│ ├── generate-unit-test.yaml # 生成单元测试的模板│ ├── generate-doc.yaml # 生成文档的模板│ └── review-code.yaml # Code Review模板├── rules/│ ├── code-style.yaml # 代码规范│ ├── naming-convention.yaml # 命名规范│ └── test-coverage.yaml # 测试覆盖率要求└── knowledge/├── common-bugs.md # 常见Bug库├── best-practices.md # 最佳实践└── faq.md # 常见问题
.claude/rules/code-style.yaml:# 团队代码规范(AI会遵循)flink:checkpoint_interval: 60000checkpoint_mode: EXACTLY_ONCEstate_backend: RocksDBrestart_strategy: fixed-delay(3, 10s)java:naming:class: PascalCasemethod: camelCaseannotations:required: [@Override, @NonNull, @Nullable]javadoc:required_for: [public_class, public_method]testing:coverage_threshold: 80required_scenarios: [normal, boundary, error]
7.6.2 让AI学习历史故障
提示词:
记录以下故障案例,加入知识库:【故障名称】:checkpoint-timeout-large-state【症状】:checkpoint耗时从30s增长到180s,最终超时失败【根因】:categoryPreference MapState没有限制大小,某些用户类目超过1000个【修复方案】:限制MapState只保留Top 10类目【相关代码】:LongTermProfileFunction.java 第87行【预防措施】:所有MapState必须设置大小限制请格式化后,写入.claude/knowledge/common-bugs.md
之后当你问Claude”为什么checkpoint会超时”时,它会自动引用这个知识库。
7.7 CI/CD集成
7.7.1 在GitHub Actions中调用AI检查
.github/workflows/flink-ci.yml:
name: Flink CI with AI Reviewon:pull_request:paths:- 'src/**/*.java'- 'src/**/*.sql'jobs:ai-review:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v3- name: Run AI Code Reviewuses: anthropic/claude-code-action@v1with:prompt: |Review this Flink code changes:- Check for missing state TTL- Check for potential data skew- Check for unit test coverage- Generate review reportapi_key: ${{ secrets.CLAUDE_API_KEY }}- name: Generate Documentationrun: |claude-code --prompt "Generate updated documentation for changed files"- name: Validaterun: mvn test
7.7.2 自动生成Release Notes
提示词:
基于Git commit历史,生成Release Notes:commit范围:v1.2.0..HEAD筛选:只包含src/main/java下的修改输出格式:- 新增功能- 性能优化- Bug修复- 配置变更- 升级注意事项生成Markdown格式。
7.8 本章小结
你学会了:
-
✅ AI辅助单元测试生成(ProcessFunction + SQL)
-
✅ AI辅助数据质量验证
-
✅ AI辅助性能基准测试
-
✅ AI辅助文档生成(运维手册 + 架构图 + API文档)
-
✅ 知识沉淀:让AI学习团队规范
-
✅ CI/CD集成
效率提升:
| 任务 | 传统方式 | AI辅助 | 提升 |
|---|---|---|---|
| 编写单元测试 | 30分钟 | 2分钟 | 15x |
| 编写运维手册 | 2小时 | 10分钟 | 12x |
| 画架构图 | 30分钟 | 1分钟 | 30x |
| 数据质量检查SQL | 20分钟 | 1分钟 | 20x |
| Code Review | 20分钟/文件 | 2分钟/文件 | 10x |
核心心法:
没有测试的代码是技术债务,没有文档的代码是个人资产。
AI让测试和文档不再是负担,而是你专业度的证明。
让AI写测试,你来思考什么场景值得测。
让AI写文档,你来确保文档真实反映代码。
7.9 动手练习
基础练习
为第4章的UserProfileJob生成单元测试:
-
让AI生成测试
-
运行测试,看覆盖率
-
补充AI遗漏的测试场景
进阶练习
为你的一个Flink作业生成完整运维手册:
-
让AI生成初版
-
根据实际情况修改
-
加入团队知识库
-
下次新人问同样问题,让他看文档
实战练习
搭建CI/CD流程:
-
在PR时自动调用AI Code Review
-
AI检查:状态TTL、单元测试、文档更新
-
如果检查不通过,自动添加评论
-
收集一周的数据:AI Review的准确率
夜雨聆风