资讯详情

AI智能体循环工程-第13章第1节-企业级综合项目-项目总览需求PR审查全自动流水线架构设计

📅 2026/10/18 5:08:44 | 华诺云谱 👁 阅读
AI智能体循环工程-第13章第1节-企业级综合项目-项目总览需求PR审查全自动流水线架构设计
第1节 项目总览需求→PR→审查全自动流水线架构设计一句话总结用一张架构图定义最终项目——票据库为脊椎、分诊/修复/审查三个子循环、看板与通知、验证门禁模块划分与技术选型的全部理由外加仓库骨架和基建代码的第一行。本文导航一、为什么是这条流水线二、总体架构五层一张图三、数据主线一张票据的一生四、模块划分与技术选型五、仓库骨架与基建代码六、踩坑实录架构层面的三个坑七、里程碑与架构自检小结一、为什么是这条流水线前面12章我把循环工程拆成了一块块零件第3章解剖了Agentic Loop的五个步骤第7章做了验证器和Maker/Checker第9章搭了SQLite票据库第10章用worktree解决了并行冲突第11章跑通了无人值守第12章补上了Evals和观测。零件都齐了但一直是散装的。这一章做一件更刺激的事把所有零件拼成一条真流水线——Issue进来PR出去中间没人。我给自己定的验收标准非常具体凌晨2点定时任务启动拉取仓库里所有未处理的GitHub Issue分诊循环判断哪些能自动修拆成带验收条件的子任务写进票据库三个修复工人并行领票各自在独立worktree里跑TDD修复循环改完的代码必须先过pytestruffmypy三件套再过双模型审查全绿才开Draft PR第二天早上我打开Slack看到的是一排PR和一份看板日报而不是一地鸡毛。为什么选Issue→PR这个场景三个原因。第一它是闭环需求有明确入口验收有明确出口天然适合用票据库串状态。第二它是刚需我维护着一个几千星的开源小项目每天睁眼十几个Issue手动triage已经把我逼疯了。第三它够综合分诊是理解型任务修复是生成型任务审查是评判型任务三种任务对模型能力的要求完全不同正好逼我把模型分层这种架构决策落到代码里。先说结论这个项目最终跑出来的数字30个真实Issue的压测集上全自动成功率87%单任务平均成本$0.06平均时长2.5分钟人工介入率13%。这些数字怎么来的后面每一节都会拆开讲。二、总体架构五层一张图架构图我用一张图说清楚。核心思想就一句话SQLite票据库是脊椎所有循环都是围着它转的器官。渲染错误:Mermaid 渲染失败: Parse error on line 26: ... LOG[日志与JSONL轨迹)] EV[Evals -----------------------^ Expecting SQE, TAGEND, UNICODE_TEXT, TEXT, TAGSTART, got CYLINDEREND五层各自的职责边界我反复强调过这里再钉一次层职责不该做的事失败会怎样输入层把外部事件变成统一格式不做任何判断事件丢失整条线饿死分诊层分类auto/semi/manual拆解子任务不写代码垃圾票进库工人空转烧钱执行层在worktree里修复自测不越scope改文件并行冲突PR互相覆盖验证层确定性三件套双模型审查不放水坏代码流出去信任崩塌交付层PR看板通知不自作主张merge人看不到结果等于白干这张表里不该做的事比职责更重要。我最惨的一次教训就是分诊循环里顺手调了一次修复API帮Issue作者看看——那次直接烧了$4多票还没写。每一层只做自己的事这是流水线不失控的第一道闸。三、数据主线一张票据的一生架构图是静态的真正让系统活起来的是票据的状态流转。整个项目里唯一有状态的实体就是票据Ticket其他模块全部无状态、可随时重启。这个设计决定让我的系统获得了两个超能力任何进程崩了重启不丢数据任何循环都可以横向扩容。分诊写票入库工人原子认领三件套全绿 审查通过 PR创建本轮修复失败重试且attempts小于3attempts达到3 升级人工人工接管处理pendingrunningdonefailedescalated状态机只有五个状态pending → running → done/failed/escalated。看着简单但有两个细节是我在实际踩坑之后才补上的第一running必须带租约lease。工人认领票据时写入自己的worker_id和认领时间其他工人看到这张票还挂在别人名下就不碰。我一开始没做租约凌晨有个工人进程OOM挂了那张票永远卡在running流水线第二天就漏票了。加了租约之后超时30分钟的running票自动回滚到pending谁都可能死系统得能自愈。第二escalated不是失败是安全出口。三次修不好就升级人工这条路径必须存在而且必须通畅否则doom loop会把预算烧穿。第3节会专门讲升级逻辑的实现。四、模块划分与技术选型4.1 模块与章节对照这个项目不是从零发明前面每一章的产出物在这里各就各位。对着下面这张表读你会看到整门课的知识是拼图关系而不是知识点列表模块职责复用章节本项目落点分诊循环Issue解析任务拆解写票第5章、第11章第3节triage_module.py修复循环TDD修复自测交回第6-7章fix_worker.py验证体系三件套双模型审查第7章validators.py、maker_checker.py票据库状态机原子认领租约第9章第5节ticket_store.pyworktree编排并行隔离第10章第3节worktree_orchestrator.py看板状态/成功率/成本聚合本章dashboard.py通知Slack/升级第11章第6节notify.pyEvals回归退化告警第12章evals/日志基建双输出按月分割第6章第1节logging_config.py这种每章一个零件、最后拼整车的结构正是循环工程这门课和普通AI教程最大的区别普通教程教你调API循环工程教你造系统。第12章讲过84%的人在用AI但只有29%敢信AI差距就在这——用的门槛是提示词信的门槛是工程。4.2 技术选型每个选择都有理由选型表我先贴出来再解释几个容易被挑战的决定组件选择理由放弃的方案语言Python 3.12生态最全、团队都会Go部署爽但LLM生态弱、Rust杀鸡牛刀包管理uv秒级装依赖、lockfile可复现poetry慢、pip无锁状态管理SQLite单文件、WAL并发安全、零运维Postgres运维成本杀鸡用牛刀、Redis内存贵且不持久LLM接入OpenAI兼容Anthropic双后端一套client跑所有模型只绑一家被锁价验证pytestruffmypy确定性信号零幻觉让LLM自评不可信通知Slack Webhook5行代码接入邮件没人看部署Docker Compose单机标准化、迁移即复制K8s本项目规模纯属自虐配置pydantic-settings环境变量类型校验yaml手解析类型裸奔两个决定我多说两句。为什么是SQLite而不是Postgres一开始我也觉得企业级就得配Postgres直到我算了一笔账这条流水线峰值也就3个工人并发读写SQLite开WAL模式后读写并发完全够单文件备份就是cp一下容器迁移零负担。而Postgres意味着多一个有状态服务、多一份运维、多一个可能崩的组件。第9章第5节那个431赞的票据库案例用的也是SQLite。当然它有边界如果我要做多机部署或每秒几百写入必须换。架构没有银弹只有匹配。为什么要双后端LLM接入因为模型分层策略第7章第7节在这个项目里是刚需maker用gpt-4o干重活checker用gpt-4o-mini做审查judge用gpt-4o-mini判验收。不同任务挂不同key不同baseurlclient必须统一抽象而且每次调用的baseurl、端口、key末6位、模型名、输入输出长度、起止时间、耗时都要进日志——这不是锦上添花这是成本核算和事后追溯的生命线。第5节压测时我能精确算出每轮调优省了多少钱全靠这行日志。五、仓库骨架与基建代码先看目录结构整个项目的骨架长这样ai-loop-pipeline/ ├── pyproject.toml # uv管理依赖锁死在uv.lock ├── docker-compose.yml # 第6节部署用 ├── .env.example # 密钥模板真实.env永不进Git ├── data/ │ └── tickets.db # SQLite票据库挂载卷持久化 ├── logs/ # 按月分割的日志 prompts/原始提示词 ├── prompts_backup/ # 每次LLM调用的完整输入输出落盘 ├── src/ai_loop_course/ │ ├── logging_config.py # 日志基建本节 │ ├── llm_client.py # LLM统一客户端第6章第2节 │ ├── ticket_store.py # 票据库状态机第9章 │ ├── triage_module.py # 分诊循环第2节 │ ├── fix_worker.py # 修复循环工人第3节 │ ├── worktree_orchestrator.py │ ├── validators.py # pytestruffmypy三件套 │ ├── maker_checker.py # 双模型审查 │ ├── dashboard.py # 看板第4节 │ └── night_pipeline.py # 总入口分诊→修复→交付 └── tests/ # 流水线自身的测试第一段要写的代码永远是日志基建。全项目所有模块都从它拿logger规范我在第6章第1节定过控制台文件双输出、按月分割、保留12个月、进程退出打印量化总结。这里给出完整实现# src/ai_loop_course/logging_config.py日志基建控制台文件双输出文件按月分割只保留最近12个月。importloggingimporttimefrompathlibimportPath LOG_DIRPath(logs)LOG_DIR.mkdir(exist_okTrue)_RETENTION12# 保留12个月历史日志classMonthlyFileHandler(logging.FileHandler):按月分割文件名直接带年月跨月写第一条日志时自动切换文件。def__init__(self,directory:Path):self._dirdirectorysuper().__init__(directory/floop-{time.strftime(%Y-%m)}.log,encodingutf-8)defemit(self,record:logging.LogRecord)-None:monthtime.strftime(%Y-%m)ifmonthnotinself.baseFilename:# 跨月了关旧文件开新文件self.close()self.baseFilenamestr(self._dir/floop-{month}.log)self.streamNonesuper().emit(record)self._cleanup()def_cleanup(self)-None:删除12个月前的历史日志文件防止磁盘被慢慢吃满。cutofftime.strftime(%Y-%m,time.localtime(time.time()-365*24*3600))forfinself._dir.glob(loop-*.log):iff.stem[-7:]cutoff:# loop-2025-08 → 2025-08f.unlink(missing_okTrue)defget_logger(name:str)-logging.Logger:全项目统一入口同名logger只挂载一次handler防止重复输出。loggerlogging.getLogger(name)iflogger.handlers:returnlogger logger.setLevel(logging.INFO)fmtlogging.Formatter(%(asctime)s | %(levelname)-7s | %(name)s | %(message)s,datefmt%Y-%m-%d %H:%M:%S,)consolelogging.StreamHandler()console.setFormatter(fmt)monthlyMonthlyFileHandler(LOG_DIR)monthly.setFormatter(fmt)logger.addHandler(console)logger.addHandler(monthly)returnlogger第二段是LLM调用的全字段日志和退出总结。llm_client.py的核心我在第6章第2节写过这里只贴本项目新增的调用摘要部分——程序退出时自动打印token和耗时的量化统计这是我自己加的下班打卡# src/ai_loop_course/llm_client.py节选调用登记与退出总结importatexitfromdataclassesimportdataclass,fielddataclassclassCallRecord:一次LLM调用的全字段记录字段与日志一一对应。model:strinput_len:int# 提示词字符数 附带文件字节数output_len:intduration:float# 秒_CALLS:list[CallRecord][]defsummarize_calls()-None:进程退出时打印量化总结token最大/最小/平均、耗时同口径。ifnot_CALLS:returnins[c.input_lenforcin_CALLS]outs[c.output_lenforcin_CALLS]durs[c.durationforcin_CALLS]nlen(_CALLS)print(*62)print(f LLM调用总结共{n}次 | 总耗时{sum(durs):.1f}s)print(f 输入长度 max{max(ins)}min{min(ins)}avg{sum(ins)//n})print(f 输出长度 max{max(outs)}min{min(outs)}avg{sum(outs)//n})print(f 单次耗时 max{max(durs):.1f}s min{min(durs):.1f}s avg{sum(durs)/n:.1f}s)print(*62)atexit.register(summarize_calls)# 无论正常退出还是跑完main都会打卡跑起来之后一次完整调用的日志长这样key只露末6位原始提示词全文落盘到prompts_backup/2026-09-29 02:14:01 | INFO | llm | 调用开始 baseurlhttps://api.openai-proxy.com:8443 key…f3a9c2 modelgpt-4o 输入3215字符0字节 2026-09-29 02:14:03 | INFO | llm | 调用成功 baseurlhttps://api.openai-proxy.com:8443 key…f3a9c2 modelgpt-4o 输入3215字符 输出842字符 开始2026-09-29 02:14:01 结束2026-09-29 02:14:03 耗时2.1s 提示词logs/prompt-20260929-021401-7c2f.txt 2026-09-29 02:14:03 | INFO | llm | 调用成功 baseurlhttps://api.openai-proxy.com:8443 key…b71e04 modelgpt-4o-mini 输入1187字符 输出403字符 开始2026-09-29 02:14:02 结束2026-09-29 02:14:03 耗时0.9s 提示词logs/prompt-20260929-021402-9d11.txt有了这两段基建后面六节的每个循环模块都是拿logger开箱即用不用重复操心观测问题。这也是我想传递的工程习惯基建先行业务后写。反过来的顺序我试过写到第4个模块就得回头重构日志代价更大。六、踩坑实录架构层面的三个坑坑1一开始设计了消息队列。我最早画架构图时塞了个RabbitMQ觉得企业级就得有MQ。真跑起来发现分诊和修复之间隔着SQLite队列里根本没有第二个消费者MQ成了纯摆设还多了个会崩的组件。教训架构图上每一条线都必须有真实的数据流动为了看起来专业而画的线最后都会变成故障点。删掉MQ后系统少了200行胶水代码。坑2验证层和执行层画混了。第一版里工人在修复循环里顺手跑pytest跑挂了又回到修复。问题是checker模型也是工人在调——相当于运动员自己当裁判模型改测试文件作弊就是从这里钻的空子第3节细讲。重构后验证层独立成门禁工人交回代码时只交diff验证器由主流程调度测试文件对工人只读。裁判必须站在运动员的循环外面这句口语化的原则比任何UML都好记。坑3忘了给分诊不写票留路径。第一版所有Issue都强制写票遇到一个纯讨论型Issue“大家觉得API该怎么设计”分诊拆出一个实现大家讨论的API子任务工人硬着头皮修了3轮失败升级人工。我早上看到票时哭笑不得。修复分诊增加manual和ignore出口不明确就生成澄清评论回贴到Issue宁可问不要猜——这条原则贯穿整个项目。七、里程碑与架构自检整章的施工顺序我排成了七个里程碑每个节点都有可验收的产物做完一个勾一个——这本身就是把循环工程用在项目管理上。七个里程碑背后是第3章第4节的目标设计公式每个里程碑都是一个可验证的成功态——M3的验收标准是抽检准确率≥90%而不是分诊能用M4是压测集75%而不是工人能跑。写不清验收标准的里程碑等于没拆。里程碑交付物验收标准M1 基建就绪logging_config llm_client日志双输出、全字段调用记录、退出总结M2 票据库ticket_store 状态机并发100次claim无一重复、租约可回收M3 分诊triage_module30个Issue抽检准确率≥90%M4 工人fix_worker maker_checker压测集成功率≥75%M5 看板dashboard 异常检测日报自动推SlackM6 部署Dockerfile compose换机10分钟跑通M7 复盘retro脚本 路线图月度量化画像一键生成每次部署前我跑同一个60秒自检脚本逐项确认五层的生命体征任何一项不过就不放行主流程# scripts/health.py架构自检票据库/日志/LLM/GitHub/Slack逐项检查全过才exit 0。importsqlite3frompathlibimportPathfromai_loop_course.configimportsettingsfromai_loop_course.logging_configimportget_logger loggerget_logger(health)CHECKS:list[tuple[str,bool,str]][]defcheck(name:str,fn)-None:跑一项检查异常记❌正常记✅并保留细节。try:CHECKS.append((name,True,fn()))exceptExceptionase:CHECKS.append((name,False,str(e)[:100]))defping_llm()-str:LLM连通性发一次1 token的最小请求验证baseurlkey。fromai_loop_course.llm_clientimportLLMClient LLMClient(settings).generate(ping,modelgpt-4o-mini,max_tokens1)returnfbaseurl{settings.baseurl}:{settings.port}key…{settings.key[-6:]}可用check(票据库,lambda:f{sqlite3.connect(settings.db_path).execute(SELECT COUNT(*) FROM tickets).fetchone()[0]}张票可读)check(日志目录,lambda:(Path(logs).mkdir(exist_okTrue),logs/可写)[1])check(LLM连通,ping_llm)check(GitHub,lambda:gh_rate_limit(settings.github_token))check(Slack,lambda:slack_test(settings.slack_webhook))forname,ok,detailinCHECKS:print(f{✅ifokelse❌}{name}{detail})(logger.infoifokelselogger.error)(f自检{name}{detail})raiseSystemExit(1ifany(notokfor_,ok,_inCHECKS)else0)✅ 票据库 208张票可读 ✅ 日志目录 logs/可写loop-2026-09.log 按月分割正常 ✅ LLM连通 baseurlhttps://api.openai-proxy.com:8443 key…f3a9c2 可用 ✅ GitHub token有效ratelimit剩余4992 ✅ Slack webhook有效#ai-pipeline 2026-09-29 01:59:00 | INFO | health | 自检完成5/5通过放行流水线自检不过不放行这条规则救过我一次某天凌晨LLM代理证书过期没有自检的话分诊会整批失败全部走兜底转manual我早上要手动补30个Issue的分诊。有自检流水线在第一步停下并发了Slack告警证书续上后手动重跑10分钟恢复。流水线的第一行代码应该是确认自己能干活而不是开始干活——这和工人领票前先验租约是同一个哲学。小结五层架构输入→分诊→执行→验证→交付SQLite票据库是脊椎所有循环围绕它转其他模块全部无状态可重启。票据状态机pending→running→done/failed/escalated五状态running带租约防漏票escalated是安全出口不是失败。选型克制SQLite而非Postgres、Compose而非K8s——每一条不用更重的方案的决定都让系统少一个会崩的组件。基建先行日志按月分割留12个月、LLM调用全字段记录、原始提示词落盘、退出打印量化总结这些在写第一个业务模块之前就要就位。下节预告架构图定了脊椎立了下一节开始造第一个器官第13章第2节《需求分诊模块Issue解析与任务拆解循环》——拉取Issue、判断可自动化性、拆解成验收条件明确的子任务、写票入库分诊提示词与pydantic结构化输出的完整实现。如果觉得本文对你有帮助欢迎点赞、收藏、关注三连本系列持续更新中关注不迷路~文章编号第13章第1节 | 总进度91/120 | 预计阅读时间11分钟
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑