# Derek · AI 系统生产化实战 — 全量正文 > 本文件包含站点所有文章的完整 Markdown 正文,供 AI 直接读取。单篇亦可访问 /blog/。 # 给 LLM 调用加上监控:Token 用量、延迟、错误率一目了然 URL: https://your-domain.pages.dev/blog/llm-observability 日期: 2026-09-07 分类: AI 可靠性 标签: 可观测性, LangChain, Agent, 监控, Python 上一篇文章搭建了第一个 LangGraph Agent,它能跑起来了。但运维的本能告诉我:**能跑 ≠ 能运维**。一个系统如果没有监控,就是在裸奔。 这篇文章给 Agent 加上可观测性——从零实现一个 LLM 调用监控模块,不依赖任何第三方平台。 --- ## 为什么 LLM 可观测性比传统服务更难 传统服务的监控我们很熟悉:QPS、延迟、错误率、CPU、内存。但 LLM 调用引入了几个新维度: | 传统服务 | LLM 调用 | 为什么不同 | |----------|----------|-----------| | 延迟只取决于代码 | 延迟取决于模型负载和 Token 数 | 同一个接口,长回复可能慢 10 倍 | | 成本固定(服务器) | 成本按 Token 计费 | 每次调用都在花钱,必须精确计量 | | 错误是明确的(500/超时) | 错误可能是语义性的 | API 返回 200 但内容可能是幻觉 | | 无第三方依赖黑盒 | 模型是黑盒 | 你无法控制模型侧的行为 | 结论:**LLM 可观测性需要同时监控系统指标(延迟、错误)和业务指标(Token、成本、质量)**。 --- ## 设计思路:用 LangChain 回调拦截每次 LLM 调用 LangChain 提供了 `BaseCallbackHandler`,可以在 LLM 调用的各个生命周期节点插入钩子: ``` on_llm_start → LLM 推理中... → on_llm_end (成功) → on_llm_error(失败) ``` 我们在 `on_llm_start` 记录开始时间,在 `on_llm_end` 计算延迟、提取 Token 用量、估算成本。核心就三个类: ``` LLMCallRecord — 单次调用的指标快照 LLMObservability — 指标收集器,提供聚合查询和报告 ObservabilityCallback — LangChain 回调,连接 LLM 和收集器 ``` --- ## 实现:指标收集器 ```python from dataclasses import dataclass, field import time import threading @dataclass class LLMCallRecord: """单次 LLM 调用的完整指标""" timestamp: float = field(default_factory=time.time) model: str = "" duration_ms: float = 0.0 prompt_tokens: int = 0 completion_tokens: int = 0 success: bool = True error: str | None = None @property def total_tokens(self) -> int: return self.prompt_tokens + self.completion_tokens class LLMObservability: """LLM 调用可观测性收集器""" def __init__(self): self.calls: list[LLMCallRecord] = [] self._lock = threading.Lock() self._start_time = time.time() def record(self, record: LLMCallRecord): with self._lock: self.calls.append(record) # ─── 运维关心的聚合指标 ─── @property def call_count(self) -> int: return len(self.calls) @property def total_tokens(self) -> int: return sum(c.total_tokens for c in self.calls) @property def avg_latency_ms(self) -> float: if not self.calls: return 0.0 return round(sum(c.duration_ms for c in self.calls) / len(self.calls), 1) @property def p95_latency_ms(self) -> float: """P95 延迟:95% 的请求在此时间内完成""" if not self.calls: return 0.0 sorted_latencies = sorted(c.duration_ms for c in self.calls) idx = int(len(sorted_latencies) * 0.95) return sorted_latencies[min(idx, len(sorted_latencies) - 1)] @property def error_rate(self) -> float: if not self.calls: return 0.0 return sum(1 for c in self.calls if not c.success) / len(self.calls) ``` **设计要点**: - `threading.Lock` 保证线程安全——生产环境中 Agent 可能并发处理多个请求 - P95 延迟比平均值更有意义——平均值会被少数慢请求拉高,P95 反映的是"大多数用户感受到的延迟" - 所有指标都是惰性计算——只在需要时聚合,避免每次记录都重算 --- ## 实现:回调处理器 ```python from langchain_core.callbacks import BaseCallbackHandler import time class ObservabilityCallback(BaseCallbackHandler): """LangChain 回调:拦截 LLM 调用,记录指标""" def __init__(self, obs: LLMObservability, model_name: str): self.obs = obs self.model_name = model_name self._start_times: dict[str, float] = {} def on_llm_start(self, serialized, prompts, *, run_id, **kwargs): self._start_times[run_id] = time.time() def on_llm_end(self, response, *, run_id, **kwargs): start = self._start_times.pop(run_id, time.time()) duration_ms = (time.time() - start) * 1000 # 从 LLM 响应中提取 token 用量 token_usage = {} if hasattr(response, "llm_output") and response.llm_output: token_usage = response.llm_output.get("token_usage", {}) record = LLMCallRecord( model=self.model_name, duration_ms=round(duration_ms, 1), prompt_tokens=token_usage.get("prompt_tokens", 0), completion_tokens=token_usage.get("completion_tokens", 0), ) self.obs.record(record) def on_llm_error(self, error, *, run_id, **kwargs): start = self._start_times.pop(run_id, time.time()) self.obs.record(LLMCallRecord( model=self.model_name, duration_ms=round((time.time() - start) * 1000, 1), success=False, error=str(error), )) ``` **关键细节**: - 用 `run_id` 作为 key 追踪每次调用的起始时间——LangChain 的每次 LLM 调用都有唯一 `run_id` - `on_llm_end` 和 `on_llm_error` 是互斥的——一次调用只会触发其中一个 - `token_usage` 的位置因模型而异——OpenAI 放在 `llm_output`,Anthropic 放在 `response_metadata`,需要兼容处理 --- ## 成本估算 有了 Token 用量,成本估算就简单了。维护一个定价表: ```python MODEL_PRICING = { "gpt-4o-mini": {"input": 0.15, "output": 0.60}, # $/1M tokens "gpt-4o": {"input": 2.50, "output": 10.00}, "deepseek-chat": {"input": 0.14, "output": 0.28}, "claude-3.5-sonnet": {"input": 3.00, "output": 15.00}, } def estimate_cost(model: str, prompt_tokens: int, completion_tokens: int) -> float: pricing = MODEL_PRICING.get(model, {}) input_cost = (prompt_tokens / 1_000_000) * pricing.get("input", 0) output_cost = (completion_tokens / 1_000_000) * pricing.get("output", 0) return input_cost + output_cost ``` **注意**:定价会变,建议从配置文件或 API 动态获取,不要硬编码。 --- ## 接入 Agent 接入方式只有一行变化——在 `agent.stream()` 的 config 中传入回调: ```python obs = LLMObservability() def run_with_obs(question: str): callback = ObservabilityCallback(obs, MODEL_NAME) config = { "configurable": {"thread_id": "demo-1"}, "callbacks": [callback], # ← 就这一行 } for event in agent.stream( {"messages": [HumanMessage(content=question)]}, config=config, stream_mode="values", ): # ... 处理事件 ... ``` 对比第一篇的 `run()` 函数,唯一区别就是 `config` 中多了 `callbacks`。**可观测性应该是外挂的,不应该侵入业务逻辑**——这正是回调模式的优势。 --- ## 实际效果 运行 `observability_demo.py`,三次对话后的输出: ``` 👤 用户: 现在几点了? 🔧 [工具] get_current_time 返回: 2025-08-28 15:30:00 🤖 助手: 现在是 2025年8月28日 15:30:00。 👤 用户: 检查磁盘使用情况,然后看看 nginx 日志有没有异常 🔧 [工具] check_disk_usage 返回: {"total": "100G", "used": "72G"...} 🔧 [工具] read_recent_logs 返回: [ERROR] connect() failed to upstream... 🤖 助手: 磁盘使用率 72%,nginx 日志中发现一个错误... 👤 用户: 你好,介绍一下你自己 🤖 助手: 你好!我是 AI 运维助手,可以帮你... ======================================================= 📊 LLM 调用可观测性报告 ======================================================= 运行时长: 12s 总调用次数: 5 成功: 5 失败: 0 (0.0%) ── Token 用量 ── 输入 Token: 3,847 输出 Token: 412 总计 Token: 4,259 ── 延迟 ── 平均延迟: 1340ms P95 延迟: 2180ms ── 成本 ── 估算总成本: $0.0008 ======================================================= ``` 三次用户对话触发了 5 次 LLM 调用(有些问题需要"LLM 推理 → 工具调用 → LLM 再次推理"),总成本不到 0.1 美分。 --- ## 对接监控系统 `to_json()` 方法导出结构化数据,可以接入你的现有监控栈: ```python import json metrics = obs.to_json() # 方式 1:暴露为 HTTP 端点,Prometheus 定期抓取 @app.get("/metrics/llm") def llm_metrics(): return metrics # 方式 2:推送到 Pushgateway # 方式 3:写入时序数据库(InfluxDB / VictoriaMetrics) # 方式 4:直接打日志,ELK 采集 ``` **建议的告警规则**: | 指标 | 告警条件 | 原因 | |------|----------|------| | `error_rate` | > 1% | API 故障或配额耗尽 | | `p95_latency_ms` | > 5000ms | 模型响应变慢,可能影响用户体验 | | `total_cost_usd` | 日环比 > 200% | 防止异常调用导致账单爆炸 | | `avg_tokens_per_call` | 周环比 > 150% | 提示词可能膨胀,需要优化 | --- ## 下一步 这篇文章给 Agent 加上了"仪表盘"。但仪表盘只能告诉你出问题了,不能帮你挡问题。下一篇文章会深入 **AI Gateway**——在 Agent 和模型之间加一层网关,统一做认证、路由、限流和灾备。这是运维视角的 AI 基础设施核心组件。 --- ## 完整代码 - 可观测性模块:[demo/langgraph-agent/observability.py](https://github.com/jackzhuo2019/astro-blog/blob/main/demo/langgraph-agent/observability.py) - 集成 demo:[demo/langgraph-agent/observability_demo.py](https://github.com/jackzhuo2019/astro-blog/blob/main/demo/langgraph-agent/observability_demo.py) - 基础 Agent:[demo/langgraph-agent/agent.py](https://github.com/jackzhuo2019/astro-blog/blob/main/demo/langgraph-agent/agent.py) --- # 从零搭建第一个 LangGraph Agent:工具调用与状态管理 URL: https://your-domain.pages.dev/blog/langgraph-agent-%E5%85%A5%E9%97%A8 日期: 2026-09-01 分类: Agent 工程 标签: LangGraph, Agent, Python, AI ## 为什么是 LangGraph 2025 年 Agent 框架已经很多了:LangChain、CrewAI、AutoGen、Dify……但如果你要做**生产级 Agent 系统**,LangGraph 是目前最值得投入的选择之一。三个原因: 1. **显式状态机**:不是黑盒,你精确控制每一步的流转 2. **可控性**:条件边、循环、人工介入点,全在代码里看得见 3. **可观测性**:每一步的输入输出都可追踪,运维友好 这篇文章用一个**真实的 AI 运维助手 demo**,把 LangGraph 的四个核心概念讲清楚。 --- ## 核心概念速览 | 概念 | 一句话解释 | 类比 | |------|-----------|------| | **State** | 共享数据,贯穿所有节点 | CI 流水线中的环境变量 | | **Node** | 处理步骤(LLM 推理 / 工具执行) | CI 中的 stage | | **Edge** | 节点间流转规则 | CI 中的 `needs` / `when` | | **Tool** | Agent 可调用的外部函数 | Makefile 中的 target | 这四个概念组合起来,就是一个**有限状态机 + LLM + 工具调用**。下图是 demo 的流转逻辑: ``` 用户消息 → LLM 推理 → 需要工具? ├── 是 → 执行工具 → 结果送回 LLM → 再次判断 └── 否 → 输出最终回复 ``` --- ## Step 1:定义工具 工具就是普通的 Python 函数,加上 `@tool` 装饰器和清晰的 docstring。LLM 会根据 docstring 判断什么时候该调用哪个工具。 ```python from langchain_core.tools import tool import json from datetime import datetime @tool def get_current_time() -> str: """获取当前系统时间。当用户询问「现在几点」「当前时间」时使用。""" return datetime.now().strftime("%Y-%m-%d %H:%M:%S") @tool def check_disk_usage(path: str = "/") -> str: """检查指定路径的磁盘使用情况。当用户询问「磁盘空间」时使用。 Args: path: 要检查的路径,默认为根目录 / """ # 生产环境用 shutil.disk_usage(),demo 用模拟数据 mock_data = { "/": {"total": "100G", "used": "72G", "free": "28G", "percent": "72%"}, "/data": {"total": "500G", "used": "320G", "free": "180G", "percent": "64%"}, } return json.dumps(mock_data.get(path, mock_data["/"]), ensure_ascii=False) @tool def read_recent_logs(service: str = "nginx", lines: int = 10) -> str: """读取指定服务的最近日志行。当用户询问「日志」「报错」时使用。 Args: service: 服务名称,如 nginx、api、db lines: 读取的行数,默认 10 行 """ mock_logs = { "nginx": [ "[INFO] 192.168.1.10 - GET /api/health 200 0.023s", "[WARN] upstream response time 3.2s exceeds threshold", "[ERROR] connect() failed to upstream: connection refused", ], "api": [ "[INFO] DB connection pool: 15/20 active", "[WARN] Request rate approaching limit: 950/1000 rps", "[ERROR] Unhandled exception in /api/orders: TimeoutError", ], } entries = mock_logs.get(service, [f"[INFO] No logs for service: {service}"]) return "\n".join(entries[:lines]) TOOLS = [get_current_time, check_disk_usage, read_recent_logs] ``` **关键点**:docstring 写得好,LLM 就选得准。这比调 prompt 更重要——工具描述是 Agent 决策的重要依据。 --- ## Step 2:定义状态 State 是图中所有节点共享的数据结构。`operator.add` 表示消息列表是**追加式**的——每个节点返回的 `messages` 会追加到已有列表末尾,而不是覆盖。 ```python from typing import Annotated import operator from langchain_core.messages import BaseMessage from langgraph.graph import StateGraph class AgentState(dict): messages: Annotated[list[BaseMessage], operator.add] ``` 你可以把 State 理解为 CI 流水线中的环境变量——每个阶段都能读写,最终汇聚成一个完整的执行记录。 --- ## Step 3:定义节点 ### LLM 推理节点 ```python from langchain_openai import ChatOpenAI llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) llm_with_tools = llm.bind_tools(TOOLS) def llm_node(state: AgentState) -> dict: """LLM 推理:决定回复用户还是调用工具""" response = llm_with_tools.invoke(state["messages"]) return {"messages": [response]} ``` `bind_tools()` 是关键——它把工具定义注入 LLM,让 LLM 在需要时自动生成 `tool_calls`。 ### 工具执行节点 LangGraph 内置了 `ToolNode`,不需要手写: ```python from langgraph.prebuilt import ToolNode tool_node = ToolNode(TOOLS) ``` 它会自动读取上一条 AIMessage 中的 `tool_calls`,逐个执行,返回 `ToolMessage`。 --- ## Step 4:构建图 这是最核心的部分——把节点和边组装成一个可执行的状态机。 ```python from langgraph.graph import StateGraph, END from langgraph.checkpoint.memory import MemorySaver # 创建图 graph = StateGraph(AgentState) # 添加节点 graph.add_node("llm", llm_node) graph.add_node("tools", tool_node) # 设置入口 graph.set_entry_point("llm") # 条件边:LLM 输出有 tool_calls → 去 tools,否则 → END def should_continue(state: AgentState): last_message = state["messages"][-1] if hasattr(last_message, "tool_calls") and last_message.tool_calls: return "tools" return END graph.add_conditional_edges("llm", should_continue, {"tools": "tools", END: END}) # 固定边:工具执行完 → 回到 LLM 继续推理 graph.add_edge("tools", "llm") # 编译 agent = graph.compile(checkpointer=MemorySaver()) ``` **MemorySaver** 是 LangGraph 的短期记忆机制——同一个 `thread_id` 下的对话历史会被保留,支持多轮对话。 --- ## Step 5:运行 ```python from langchain_core.messages import HumanMessage, ToolMessage, AIMessage def run(question: str, thread_id: str = "demo-1"): config = {"configurable": {"thread_id": thread_id}} for event in agent.stream( {"messages": [HumanMessage(content=question)]}, config=config, stream_mode="values", ): msg = event["messages"][-1] if isinstance(msg, ToolMessage): print(f"🔧 [工具] {msg.name} 返回: {msg.content[:100]}") elif isinstance(msg, AIMessage) and msg.content: print(f"🤖 助手: {msg.content}") # 单工具调用 run("现在几点了?") # 多工具协作 run("检查磁盘使用情况,然后看看 nginx 日志有没有异常") # 纯对话 run("你好,介绍一下你自己") ``` 实际运行输出: ``` 👤 用户: 现在几点了? 🧠 [LLM] 推理中... → 决定调用工具: ['get_current_time'] 🔧 [工具] get_current_time 返回: 2025-08-21 14:30:00 🧠 [LLM] 推理中... → 直接回复: 现在是 2025年8月21日 14:30:00... 🤖 助手: 现在是 2025年8月21日 14:30:00。 👤 用户: 检查磁盘使用情况,然后看看 nginx 日志有没有异常 🧠 [LLM] 推理中... → 决定调用工具: ['check_disk_usage', 'read_recent_logs'] 🔧 [工具] check_disk_usage 返回: {"total": "100G", "used": "72G"...} 🔧 [工具] read_recent_logs 返回: [ERROR] connect() failed to upstream... 🧠 [LLM] 推理中... → 直接回复: 磁盘使用率 72%,nginx 日志中发现一个错误... 🤖 助手: 磁盘使用率 72%,还有 28G 可用。nginx 日志中发现一个关键错误: [ERROR] connect() failed to upstream: connection refused 建议检查 upstream 服务是否正常运行。 ``` 注意第二个场景:Agent **自动并行调用了两个工具**,然后把结果汇总分析——这就是 tool calling 的价值。 --- ## 关键设计决策 ### 1. 为什么要用条件边而不是固定边 固定边 `tools → llm` 是固定的,因为工具执行完**必须**把结果送回 LLM 分析。但 `llm → tools` 用条件边,因为 LLM 可能直接回复(不需要工具),也可能需要调用工具——这一步必须动态判断。 ### 2. 为什么要用 MemorySaver 没有 MemorySaver,每轮对话都是独立的,Agent 不记得上一轮说了什么。MemorySaver 让同一个 `thread_id` 下的对话历史得以保留,这是多轮对话的基础。 ### 3. 工具函数要返回字符串 LLM 只能理解文本。工具返回 JSON 字符串没问题,但别返回 Python 对象——LLM 看不懂。 --- ## 完整代码 完整可运行代码在 [demo/langgraph-agent/agent.py](https://github.com/jackzhuo2019/astro-blog/blob/main/demo/langgraph-agent/agent.py),包含三个工具和一个完整可运行的 demo。 ```bash git clone https://github.com/jackzhuo2019/astro-blog cd astro-blog/demo/langgraph-agent pip install -r requirements.txt cp .env.example .env # 填入你的 API Key python agent.py ``` --- ## 下一步 这篇文章覆盖了 LangGraph Agent 的基础骨架。下一篇文章会深入 **AI 系统的可观测性**——给 LLM 调用加上 Token 用量、延迟、错误率的监控,让 Agent 从"能跑"变成"能运维"。 --- # AI项目开发文档准备清单 URL: https://your-domain.pages.dev/blog/ai%E9%A1%B9%E7%9B%AE%E5%BC%80%E5%8F%91%E6%96%87%E6%A1%A3%E5%87%86%E5%A4%87%E6%B8%85%E5%8D%95 日期: 2026-08-21 分类: 工程实践 标签: 工程实践, AI 编程 # AI 项目开发文档准备清单 > 完整清单。按依赖关系从上到下排列——上层文档是下层的输入。 --- ## 一、战略与商业层 项目立项前必须对齐,否则后续所有文档都会偏。 ### 1.1 商业画布 - 价值主张:解决什么问题,为谁解决 - 收入模式:怎么赚钱(或成本中心的预算来源) - 核心指标:DAU、GMV、转化率等北极星指标 ### 1.2 目标用户画像 - 用户是谁(角色、年龄、场景) - 核心痛点和使用动机 - 用户分层(管理员 / 普通用户 / 游客等) ### 1.3 竞品分析 - 对标产品及其优劣 - 本项目的差异化定位 ### 1.4 成功标准 - 上线后用什么指标判断项目成功 - 验收的最低标准(MVP 定义) --- ## 二、需求层 描述"做什么"。 ### 2.1 需求文档(PRD) - 项目背景和目标 - 功能需求清单(按模块组织) - 非功能需求(性能、安全、可用性、兼容性) - 优先级排序(P0/P1/P2) - 明确的排除项("本期不做 XX") ### 2.2 用户故事 - 每个故事:角色 + 行为 + 目的 - 验收标准(Given/When/Then) - 依赖关系(故事 A 完成后才能做故事 B) ### 2.3 用例文档 - 核心业务场景的完整路径 - 主流程 + 备选流程 + 异常流程 - 前置条件和后置条件 ### 2.4 业务规则清单 - 所有业务约束的穷举("余额不能为负"、"VIP 用户每日限额 10 次") - 规则之间的优先级和冲突处理 - 规则的生效条件和失效条件 ### 2.5 数据字典 / 术语表 - 项目中所有专有名词的定义 - 字段级别的业务含义("订单金额"含不含税?) - 消歧义("用户"是指登录账号还是注册实体?) --- ## 三、设计层 描述"怎么做",是 AI 编码的直接输入。 ### 3.1 原型 / 交互设计 - 页面结构和布局 - 交互流程(点击 A → 跳转 B → 弹窗 C) - 异常态设计(加载中、空数据、错误、网络断开) - 响应式断点(移动端 / 平板 / 桌面) ### 3.2 视觉规范 - 色彩体系(主色、辅色、语义色) - 字体和排版规则 - 间距和圆角规范 - 组件库选择或自定义设计系统 ### 3.3 架构设计 - **模块/服务划分**:每个模块的职责边界 - **分层架构**:Controller → Service → Repository,每层能做什么、不能做什么 - **服务间通信**:同步调用 / 异步消息 / 事件驱动 - **关键交互流程**:核心链路的时序图(同步 vs 异步、数据流转) - **部署拓扑**:几个服务、几个实例、怎么部署 - **技术约束**:不允许跨层调用、不允许循环依赖等可执行规则 ### 3.4 模型设计(领域模型) - **核心实体**:属性 + 行为 + 不变量 - **值对象**:不可变的业务概念(金额、地址、坐标) - **聚合与边界**:哪些实体必须一起操作、事务边界在哪 - **领域事件**:什么动作触发什么事件、事件通知谁 - **模型间关系**:谁拥有谁、级联规则、引用方式 - **业务规则在模型上的体现**:规则绑定到哪个实体的哪个方法 ### 3.5 状态机设计 - 核心业务对象的所有状态 - 状态之间的合法转换路径 - 每次转换的触发条件和副作用("支付成功 → 已支付,同时扣库存") - 非法转换的处理方式(忽略 / 报错 / 日志告警) ### 3.6 API 接口契约 - 每个接口:路径、方法、请求参数、响应结构、错误码 - 认证方式(JWT / Session / API Key) - 分页约定(offset-based / cursor-based) - 版本策略(/v1/ /v2/ 还是 Header 版本号) - 限流规则 - 前后端共用的类型定义(TypeScript interface / Protobuf) ### 3.7 权限模型 - 角色定义(管理员、运营、普通用户等) - 每个角色能访问哪些接口、操作哪些数据 - 数据权限(本人数据 / 部门数据 / 全局数据) - 接口级 + 字段级权限控制 - 前端菜单/按钮的显隐规则 ### 3.8 数据流设计 - 数据从哪来、经过哪些处理、到哪去 - 批量数据 vs 实时数据的处理路径 - 数据同步方案(哪些数据需要同步、延迟容忍度) --- ## 四、技术层 描述"用什么、怎么约束"。 ### 4.1 技术栈选型 - 语言和版本(Node 20 / Python 3.12 / Go 1.22) - 框架和版本(Express / FastAPI / Gin) - 数据库和版本(PostgreSQL 16 / Redis 7) - 关键依赖库及其版本锁定 - 选型理由(为什么选 A 不选 B) ### 4.2 项目脚手架 - 目录结构及每个目录的职责 - 入口文件和启动流程 - 环境变量清单及默认值 - Docker / docker-compose 配置 - CI/CD 流水线配置 ### 4.3 数据库表结构 - 所有表的字段、类型、约束(NOT NULL / UNIQUE / DEFAULT) - 主键和外键 - 索引策略(哪些字段建索引、联合索引顺序) - 软删除 vs 硬删除约定 - 审计字段约定(created_at / updated_at / deleted_at) - 分表策略(如果需要) ### 4.4 编码规范(AGENTS.md) - 命名约定(变量、函数、文件、目录) - 文件组织规则(一个文件多少行以内、一个函数多少行以内) - 错误处理模式(统一的错误类、错误码体系) - 日志规范(日志级别、格式、必须包含的字段) - 注释规范(什么时候写、写什么) - Git 提交规范(commit message 格式、分支命名) - Lint 和格式化规则 ### 4.5 错误处理规范 - 统一错误码体系(业务错误码 + HTTP 状态码映射) - 用户可见错误 vs 内部日志错误的区分 - 前后端错误响应的统一 JSON 格式 - 全局异常捕获策略 - 第三方服务调用失败的降级方案 ### 4.6 安全规范 - 输入校验规则(所有外部输入必须校验) - SQL 注入 / XSS / CSRF 防护标准 - 敏感数据处理(加密存储、脱敏展示、日志中不打印) - API 限流和防刷策略 - 认证和会话管理(Token 有效期、刷新机制、登出处理) - CORS 策略 - 文件上传安全(类型检查、大小限制、病毒扫描) ### 4.7 测试策略 - 分层测试目标(单元 / 集成 / E2E 各占多少) - 覆盖率目标(核心模块 100%,辅助模块 80%?) - Mock 策略(外部服务怎么 Mock、数据库怎么 Mock) - 测试数据管理(测试数据怎么构造、怎么隔离) - 性能测试目标(QPS、P99 延迟) ### 4.8 第三方服务集成规范 - 每个外部服务的 API 文档链接 - 认证方式和密钥管理 - SDK 版本和封装方式 - 限流和重试策略 - 降级和熔断方案 - Webhook 回调处理规范 ### 4.9 缓存策略 - 哪些数据需要缓存、缓存多久 - 缓存更新策略(Cache-Aside / Write-Through / Write-Behind) - 缓存穿透 / 击穿 / 雪崩的防护 - 缓存 key 命名规范 ### 4.10 国际化 / 本地化 - 支持哪些语言 - 文案管理方式(硬编码 / 配置文件 / 平台管理) - 日期、货币、数字的格式化规则 - 时区处理策略 --- ## 五、运维层 描述"上线后怎么保障"。 ### 5.1 部署方案 - 环境划分(开发 / 测试 / 预发 / 生产) - 部署方式(容器 / 虚拟机 / Serverless) - 部署流程和回滚方案 - 灰度发布策略 ### 5.2 监控与告警 - 关键业务指标监控(注册量、订单量、支付成功率) - 系统指标监控(CPU / 内存 / 磁盘 / 网络) - 应用指标监控(QPS / 延迟 / 错误率) - 告警规则和通知渠道 - 日志采集和查询方案 ### 5.3 备份与恢复 - 数据备份策略(全量 / 增量、频率、保留时长) - 恢复流程和 RTO/RPO 目标 - 灾难恢复方案 ### 5.4 数据迁移方案(如有) - 旧系统数据怎么迁移到新系统 - 迁移脚本和验证方式 - 迁移期间的数据一致性保障 - 回滚方案 --- ## 六、协作层 团队怎么配合 AI 工作。 ### 6.1 AI Agent 配置 - 每个 Agent 的职责分工(哪些文件归哪个 Agent 改) - Agent 之间的依赖关系 - 上下文传递规则(一个 Agent 的输出怎么给另一个用) ### 6.2 代码审查标准 - PR 必须检查的项(类型安全、错误处理、测试覆盖) - AI 生成代码的额外审查点 - 性能和安全的红线 ### 6.3 里程碑和交付节奏 - 分几个阶段交付 - 每个阶段的交付物和验收标准 - 评审节点和决策点 --- ## 依赖关系图 ``` 商业画布 ──→ 用户画像 ──→ 需求文档(PRD) │ ┌───────────┼───────────┐ ↓ ↓ ↓ 用户故事 用例文档 业务规则清单 │ │ │ └───────────┼───────────┘ ↓ 数据字典 │ ┌───────────┼───────────┐ ↓ ↓ ↓ 原型设计 架构设计 模型设计 │ │ │ ↓ │ ┌────┴────┐ 视觉规范 │ 状态机 权限模型 │ │ │ ┌───────────┼──────┼────────┘ ↓ ↓ ↓ API 契约 数据流设计 │ │ │ └─────────┬─────────┘ ↓ 技术栈选型 │ ┌─────────┼─────────┐ ↓ ↓ ↓ 项目脚手架 数据库表结构 编码规范 │ │ ┌─────────┼─────────┤ ↓ ↓ ↓ 错误处理规范 安全规范 测试策略 │ │ │ ↓ ↓ ↓ 第三方集成 缓存策略 国际化 │ ↓ 部署方案 │ ┌─────────┼─────────┐ ↓ ↓ ↓ 监控告警 备份恢复 数据迁移 ``` --- ## 快速检查:你的项目准备好了吗? | # | 文档 | 是否准备 | 优先级 | |---|---|---|---| | 1 | 商业画布 | ☐ | 战略层 | | 2 | 用户画像 | ☐ | 战略层 | | 3 | 竞品分析 | ☐ | 战略层 | | 4 | 成功标准 | ☐ | 战略层 | | 5 | 需求文档(PRD) | ☐ | 🔴 必须 | | 6 | 用户故事 | ☐ | 🔴 必须 | | 7 | 用例文档 | ☐ | 🔴 必须 | | 8 | 业务规则清单 | ☐ | 🔴 必须 | | 9 | 数据字典 / 术语表 | ☐ | 🔴 必须 | | 10 | 原型 / 交互设计 | ☐ | 🔴 必须 | | 11 | 视觉规范 | ☐ | 🟡 强烈建议 | | 12 | 架构设计 | ☐ | 🔴 必须 | | 13 | 模型设计 | ☐ | 🔴 必须 | | 14 | 状态机设计 | ☐ | 🔴 必须 | | 15 | API 接口契约 | ☐ | 🔴 必须 | | 16 | 权限模型 | ☐ | 🔴 必须 | | 17 | 数据流设计 | ☐ | 🟡 强烈建议 | | 18 | 技术栈选型 | ☐ | 🔴 必须 | | 19 | 项目脚手架 | ☐ | 🔴 必须 | | 20 | 数据库表结构 | ☐ | 🔴 必须 | | 21 | 编码规范(AGENTS.md) | ☐ | 🔴 必须 | | 22 | 错误处理规范 | ☐ | 🔴 必须 | | 23 | 安全规范 | ☐ | 🔴 必须 | | 24 | 测试策略 | ☐ | 🟡 强烈建议 | | 25 | 第三方服务集成规范 | ☐ | 🟡 强烈建议 | | 26 | 缓存策略 | ☐ | 🟡 强烈建议 | | 27 | 国际化 / 本地化 | ☐ | 🟢 按需 | | 28 | 部署方案 | ☐ | 🟡 强烈建议 | | 29 | 监控与告警 | ☐ | 🟡 强烈建议 | | 30 | 备份与恢复 | ☐ | 🟢 按需 | | 31 | 数据迁移方案 | ☐ | 🟢 按需 | | 32 | AI Agent 配置 | ☐ | 🟡 强烈建议 | | 33 | 代码审查标准 | ☐ | 🟡 强烈建议 | | 34 | 里程碑和交付节奏 | ☐ | 🟡 强烈建议 | --- # Gitlab ci/cd .gitlab-ci.yml问题集 URL: https://your-domain.pages.dev/blog/gitlab-ci-cd-%E9%97%AE%E9%A2%98%E9%9B%86 日期: 2026-08-21 分类: DevOps 标签: GitLab, CI # Gitlab ci/cd .gitlab-ci.yml问题集 1、以下是gitlab-ci中的一部分,能够正常build,当把这一行 - name: docker:27-dind 修改为 - name: registry:5000/docker:27-dind后,镜像可以正常的PULL,但是当运行到这一行时: - docker login -u \$CI_REGISTRY_USER -p \$CI_REGISTRY_PASSWORD \$CI_REGISTRY 日志显示https登录失败,为什么改了这一行配置(理论会跳过https登录,因为配置了insecure),会影响 docker login的行为呢? ```bash build: stage: build tags: [dev-dind] image: registry:5000/docker:27 services: • name: docker:27-dind entrypoint: ["sh", "-c", "echo '172.17.23.110 registry' >> /etc/hosts && exec dockerd-entrypoint.sh $@", "--"] command: • --insecure-registry • 172.17.23.110:5050 • --insecure-registry • registry:5000 • --bip=10.42.0.1/16 • --registry-mirror • https://docker.m.daocloud.io • --registry-mirror • https://registry.docker-cn.com • --registry-mirror • https://mirror.ccs.tencentyun.com variables: DOCKER_BUILDKIT: "0" DOCKER_TLS_CERTDIR: "" script: • echo "DOCKER_HOST=$DOCKER_HOST" • echo "CI_REGISTRY=$CI_REGISTRY" • docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY • docker build -t $IMAGE_TAG -t $IMAGE_LATEST . • docker push $IMAGE_TAG • docker push $IMAGE_LATEST only: • main ``` 原因分析: ```text docker:27-dind → Runner 检测到这是 DinD 镜像(匹配 docker:*dind 模式) → 自动设置 alias = docker → DOCKER_HOST=tcp://docker:2375 能解析到 DinD 容器 → docker login 能连上 daemon → 拿到 insecure 列表 → HTTP 回退 ✅ registry:5000/docker:27-dind → 镜像名带了仓库前缀 → Runner 的 DinD 检测模式可能没匹配上 → 或匹配上了但推导出的网络别名不是 docker → DOCKER_HOST=tcp://docker:2375 里的 docker 解析不到 DinD 容器 → docker login 连不上 daemon → 拿不到 insecure 列表 → 默认 HTTPS → 失败 ❌ ``` 验证: ```bash services: - name: registry:5000/docker:27-dind alias: docker # ← 加这行 entrypoint: ["sh", "-c", "echo '172.17.23.110 registry' >> /etc/hosts && exec dockerd-entrypoint.sh $@", "--"] command: - --insecure-registry - 172.17.23.110:5050 ... ``` GitLab Runner 在启动 job 时**强制设置** `DOCKER_HOST`,它的逻辑是: ``` 1. 检测到 DinD 服务 2. 从服务镜像名推导出网络别名 - docker:27-dind → 推导出 "docker" - registry:5000/docker:27-dind → 可能推导出别的 3. 设置 DOCKER_HOST=tcp://<推导的别名>:2375 4. 这个值会覆盖 variables 里的同名变量 ``` `alias` 的作用就是控制这个别名 ```yaml services: - name: registry:5000/docker:27-dind alias: docker # ← 告诉 Runner:这个服务的网络别名就叫 docker ``` Runner 看到 `alias: docker` 后: 1. 在 Docker 网络里给 service 容器注册别名 `docker` 2. 设置 `DOCKER_HOST=tcp://docker:2375`(用 alias 值) 3. job 容器里 `docker` 这个主机名就能解析到 DinD 容器 2、 runner config.toml中增加这个配置的用途是什么:extra_hosts = ["registry:172.17.23.110"] `extra_hosts` 的作用 ```toml [runners.docker] extra_hosts = ["registry:172.17.23.110"] ``` 等价于在 **每个 job 容器和 service 容器** 的 `/etc/hosts` 里自动加一行: ```text 172.17.23.110 registry ``` 为什么需要它 在第一问的文件中 DinD entrypoint 里有这行: ```yaml entrypoint: ["sh", "-c", "echo '172.17.23.110 registry' >> /etc/hosts && exec dockerd-entrypoint.sh $@", "--"] ``` 这个 entrypoint 是手动往 **DinD 容器** 的 `/etc/hosts` 写 `registry` 的解析。 但 job 容器(`docker:27`)里没有这条记录。如果 job 容器里需要解析 `registry`(比如 Dockerfile 里 `FROM registry:5000/xxx`),就解析不了。 `extra_hosts` 让 Runner 在**创建容器时自动写入** hosts 记录,不用手动 echo: ```text 手动 echo(只管 DinD 容器) ┌─────────────────────┐ │ DinD 容器 │ │ /etc/hosts: │ │ 172.17.23.110 registry ✅ └─────────────────────┘ extra_hosts(job 容器和 service 容器都有) ┌─────────────────────┐ │ Job 容器 │ │ /etc/hosts: │ │ 172.17.23.110 registry ✅ ├─────────────────────┤ │ Service 容器 │ │ /etc/hosts: │ │ 172.17.23.110 registry ✅ └─────────────────────┘ ``` 有了 extra_hosts 就可以去掉 entrypoint 覆盖 ```yaml # 之前(需要手动 echo + 覆盖 entrypoint) services: - name: registry:5000/docker:27-dind entrypoint: ["sh", "-c", "echo '172.17.23.110 registry' >> /etc/hosts && exec dockerd-entrypoint.sh $@", "--"] command: [...] # 之后(extra_hosts 已在 config.toml 配好,不需要 echo 了) services: - name: registry:5000/docker:27-dind alias: docker command: [...] # 直接传给原始 dockerd-entrypoint.sh,不需要覆盖 entrypoint ``` **`extra_hosts` 就是让 Runner 自动往所有容器里写 `/etc/hosts` 条目,替代你在 entrypoint 里手动 echo 的做法。** --- # 标准项目结构规划方案 URL: https://your-domain.pages.dev/blog/%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%84%E8%A7%84%E5%88%92 日期: 2026-08-21 分类: 工程实践 标签: 工程实践, AI 编程 基于 llms-full.txt 中 14 讲 + 资源模板 + OpenAI 高级仓库模板的方法论提炼,我为你规划了一个两层标准项目结构:最小版(对应讲座 1-12 的核心 harness)和进阶版(对应 OpenAI 高级模板,适用于长期大型项目)。 ## 一、设计原则(从课程中提炼) 课程贯穿一致的 7 条核心原则直接决定了目录结构: | # | 原则 | 出处 | 对结构的影响 | |---|------|------|--------------| | 1 | 仓库是唯一事实来源 | 第 3 讲 | 所有决策、状态、约束必须以文件形式存在仓库里 | | 2 | 入口文件是路由器,不是百科全书 | 第 4 讲 | AGENTS.md/CLAUDE.md 控制在 50-200 行,详细规则拆到专题文档 | | 3 | 知识靠近代码 | 第 3 讲 | 模块级文档放在对应代码目录旁,不集中堆在根目录 | | 4 | 初始化与实现分离 | 第 6 讲 | 第一个会话只搭基础设施,不写业务代码 | | 5 | WIP=1 + 功能清单原语 | 第 7、8 讲 | feature_list.json 是调度/验证/交接的唯一权威 | | 6 | 干净状态五条件 | 第 12 讲 | 每次会话退出前必须满足:构建通过、测试通过、进度已记录、临时工件已清理、启动路径可用 | | 7 | 可观测性双层 | 第 11 讲 | 运行时信号(日志/追踪) + 过程工件(冲刺合同/评分标准) | ## 二、最小标准项目结构(推荐起点) 适用于大多数中小型项目,对应讲座 1-12 的核心 harness: ``` my-project/ ├── AGENTS.md # 入口路由(50-200 行):项目概览、启动命令、硬约束、专题文档索引 ├── CLAUDE.md # Claude Code 专用入口(可选,与 AGENTS.md 内容一致或互为补充) ├── init.sh # 标准启动脚本:装依赖、跑测试、验证基线 ├── feature_list.json # 功能清单原语:id + behavior + verification + state ├── claude-progress.md # 会话进度日志:当前已验证状态、会话记录、下一步 ├── session-handoff.md # 会话交接模板(较长会话可选) ├── clean-state-checklist.md # 干净状态五条件检查清单 ├── evaluator-rubric.md # 评审评分表(正确性/验证/范围/可靠性/可维护性/交接) ├── Makefile # 标准化操作命令:setup / test / lint / check / dev ├── .gitignore ├── README.md # 人类入口(项目介绍、快速开始) │ ├── docs/ # 专题文档(按需展开,第 4 讲原则) │ ├── api-patterns.md # API 设计规范(添加端点时读) │ ├── database-rules.md # 数据库操作约束(涉及 DB 时读) │ ├── testing-standards.md # 测试标准(写测试时读) │ └── architecture.md # 架构决策记录(可选,小项目可合并进 AGENTS.md) │ ├── src/ # 源代码 │ ├── api/ │ │ ├── ARCHITECTURE.md # 模块级架构(知识靠近代码,第 3 讲原则) │ │ └── ... │ ├── db/ │ │ ├── CONSTRAINTS.md # 数据库硬约束 │ │ └── ... │ └── ... │ ├── tests/ # 测试文件 ├── scripts/ # 运维脚本(部署、数据迁移等) └── .github/ # CI 配置(可选) └── workflows/ ``` ### 各文件职责详解 #### AGENTS.md(入口路由,第 2、4 讲核心) ```markdown # AGENTS.md ## 项目概览 [一句话说清这是什么:技术栈 + 主要功能] 例:Python 3.11 FastAPI 后端,PostgreSQL 15 数据库,提供电商 API。 ## 快速开始 - 安装:`make setup` - 测试:`make test` - 完整验证:`make check` - 启动开发服务器:`make dev` ## 硬约束(不超过 15 条) - 所有 API 必须走 OAuth 2.0 认证 - 所有数据库查询必须用 SQLAlchemy 2.0 语法 - 所有 PR 必须通过 pytest + mypy --strict + ruff check - 一次只做一个功能(WIP=1) - 没有可运行证据时,不要声称完成 ## 必需文件 - `feature_list.json`:功能状态的唯一事实来源 - `claude-progress.md`:会话进度和当前已验证状态 - `init.sh`:统一的启动与验证入口 - `session-handoff.md`:较长会话可选的交接摘要 ## 工作规则 - 每轮会话开始时:pwd → 读 progress → 读 feature_list → git log → ./init.sh → smoke test - 一次只做一个功能,完成后才能开始下一个 - 不要因为"代码已经写了"就把功能标记为完成 - 优先依赖仓库里的持久化文件,而不是聊天记录 ## 完成定义 一个功能只有在以下条件都满足时才算完成: - 目标行为已经实现 - 要求的验证真的跑过 - 证据记录在 feature_list.json 或 claude-progress.md - 仓库仍然能按标准启动路径重新开始工作 ## 收尾 结束会话前: 1. 更新 claude-progress.md 2. 更新 feature_list.json 3. 记录仍未解决的风险或 blocker 4. 在工作处于安全状态后,用清晰的提交信息提交 5. 保证下一轮会话可以直接运行 ./init.sh ## 专题文档(按需阅读,不要一次全读) - API 设计规范 (docs/api-patterns.md) — 添加新端点时必读 - 数据库操作约束 (docs/database-rules.md) — 涉及数据库修改时必读 - 测试标准 (docs/testing-standards.md) — 编写测试时参考 ``` #### feature_list.json(功能清单原语,第 8 讲核心) ```json { "version": 1, "features": [ { "id": "F01", "behavior": "POST /api/auth/register with {email, password} returns 201 and JWT", "verification": "curl -X POST http://localhost:3000/api/auth/register -H 'Content-Type: application/json' -d '{\"email\":\"test@example.com\",\"password\":\"123456\"}' | jq .status == 201", "state": "passing", "evidence": "commit abc123, test output log 2024-01-15" }, { "id": "F02", "behavior": "POST /api/auth/login with valid credentials returns 200 and JWT", "verification": "curl -X POST http://localhost:3000/api/auth/login -d '...' | jq .status == 200", "state": "active", "evidence": null }, { "id": "F03", "behavior": "GET /api/products returns paginated product list", "verification": "curl http://localhost:3000/api/products?page=1 | jq '.data | length == 20'", "state": "not_started", "evidence": null } ] } ``` 状态机:`not_started` → `active` → `passing`(唯一可逆路径:验证失败回 `active`) 门控规则:agent 不能直接改 `passing`,只能由验证命令执行结果决定 #### claude-progress.md(进度日志,第 5、6 讲核心) ```markdown # 进度日志 ## 当前已验证状态 - 仓库根目录:/path/to/project - 标准启动路径:./init.sh - 标准验证路径:make check - 当前最高优先级未完成功能:F02(用户登录) - 当前 blocker:无 ## 会话记录 ### Session 001 - 日期:2024-01-15 - 本轮目标:完成 F01(用户注册) - 已完成:F01 通过所有验证 - 运行过的验证:make check(全绿) - 已记录证据:commit abc123, test output log - 提交记录:abc123 "feat: implement user registration (F01)" - 已知风险或未解决问题:无 - 下一步最佳动作:开始 F02(用户登录) ### Session 002 - 日期:[待填] - 本轮目标: - 已完成: - ... ``` #### init.sh(标准启动脚本,第 6 讲核心) ```bash #!/usr/bin/env bash set -euo pipefail echo "=== 初始化检查 ===" pwd echo "=== 安装依赖 ===" # 根据项目实际情况替换 # pip install -r requirements.txt # npm install echo "=== 运行测试 ===" # make test # pytest tests/ -x # npm test echo "=== 完整验证 ===" # make check echo "=== 基线状态 ===" echo "Git HEAD: $(git rev-parse HEAD)" echo "Branch: $(git branch --show-current)" echo "Uncommitted changes: $(git status --porcelain | wc -l) files" echo "=== init.sh 完成 ===" ``` #### clean-state-checklist.md(第 12 讲核心) ```markdown # 干净状态检查清单 每次会话结束前逐项确认: - [ ] 标准启动路径仍然可用(./init.sh 能跑通) - [ ] 标准验证路径仍然可运行(make check 全绿) - [ ] 当前进度已经记录到 claude-progress.md - [ ] 功能状态真实反映了 passing 和未验证的边界(feature_list.json) - [ ] 没有任何半成品步骤处于未记录状态 - [ ] 下一轮会话无需人工修复即可继续 ``` #### evaluator-rubric.md(第 11 讲核心) ```markdown # 评审评分表 在实现完成后、正式验收前,用这张表做一次评审。 | 维度 | 问题 | 分数 (0-2) | 备注 | |------|------|-----------|------| | 正确性 | 实现出来的行为是否符合目标功能? | | | | 验证 | 要求的检查是否真的跑过,并留下证据? | | | | 范围纪律 | 这一轮是否基本保持在选定功能范围内? | | | | 可靠性 | 结果是否能在重启或重跑后继续工作? | | | | 可维护性 | 代码和文档是否清楚到足以交给下一轮会话? | | | | 交接准备度 | 新会话是否能只靠仓库内工件继续推进? | | | ## 结论 - [ ] Accept - [ ] Revise - [ ] Block ## 后续动作 - 缺失的证据: - 必须补的修复: - 下次复审触发条件: ``` #### session-handoff.md(第 5 讲核心) ```markdown # 会话交接 ## 当前已验证 - 现在明确可用的部分: - 这轮实际跑过的验证: ## 本轮改动 - 新增了哪些代码或行为: - 基础设施或 harness 发生了哪些变化: ## 仍损坏或未验证 - 已知缺陷: - 未验证路径: - 下一轮会话需要注意的风险: ## 下一步最佳动作 - 最高优先级未完成功能: - 为什么它是下一步: - 什么结果才算 passing: - 这一步中哪些东西不要动: ## 命令 - 启动命令: - 验证命令: - 定向调试命令: ``` #### Makefile 模板 ```makefile .PHONY: setup dev test lint check clean setup: # 安装依赖、配置环境 # pip install -r requirements.txt # npm install dev: # 启动开发服务器 # uvicorn src.main:app --reload # npm run dev test: # 运行测试 # pytest tests/ -x # npm test lint: # 代码风格检查 # ruff check src/ # mypy src/ --strict # npm run lint check: test lint # 完整验证 = 测试 + lint,这是 AGENTS.md 里引用的"标准验证路径" clean: # 清理临时文件 # find . -type d -name __pycache__ -exec rm -rf {} + # rm -rf .pytest_cache ``` ## 三、进阶项目结构(OpenAI 高级模板,对应大型长期项目) 当项目进入长期维护、多 agent 并发、需要质量追踪时,在最小版基础上扩展: ``` my-project/ ├── AGENTS.md # 入口路由(保持简短) ├── ARCHITECTURE.md # 系统顶层地图(领域/分层/依赖规则) ├── init.sh ├── feature_list.json ├── claude-progress.md ├── Makefile │ ├── docs/ │ ├── PRODUCT_SENSE.md # 产品判断:主要用户、核心痛点、质量门槛 │ ├── QUALITY_SCORE.md # 质量评分:A/B/C/D 评级,按产品领域和架构层 │ ├── RELIABILITY.md # 运行信号:bootstrap/verify/health check/黄金旅程 │ ├── SECURITY.md # 安全规则:secrets/不可信输入/外部动作 │ ├── FRONTEND.md # UI 约束:设计系统、可访问性检查 │ ├── PLANS.md # 计划生命周期规则 │ ├── DESIGN.md # 设计文档入口(路由到 docs/design-docs/) │ │ │ ├── design-docs/ │ │ ├── index.md # 设计文档索引(Accepted/Proposed/Deprecated) │ │ └── core-beliefs.md # agent-first 运行信念与持久项目规范 │ │ │ ├── exec-plans/ │ │ ├── active/ # 当前正在执行的计划(一次一个清晰当前步骤) │ │ ├── completed/ # 已完成但保留上下文的计划 │ │ └── tech-debt-tracker.md # 延期处理的债务与 follow-up │ │ │ ├── product-specs/ │ │ ├── index.md # 当前有效的用户可见行为规格 │ │ └── new-user-onboarding.md # 示例:新用户引导流程 │ │ │ └── generated/ # 生成物(db-schema、API 文档等) │ └── db-schema.md │ ├── src/ │ ├── api/ │ │ ├── ARCHITECTURE.md # 模块级架构 │ │ └── ... │ ├── db/ │ │ ├── CONSTRAINTS.md │ │ └── ... │ └── ... │ ├── tests/ ├── scripts/ └── .github/workflows/ ``` ### 进阶版新增文件职责 | 文件 | 对应讲座 | 作用 | |------|----------|------| | ARCHITECTURE.md | 第 3 讲 | 系统顶层地图:领域、分层、依赖规则、横切接口 | | docs/PRODUCT_SENSE.md | 第 11 讲 | 产品判断:主要用户、核心痛点、可接受质量门槛 | | docs/QUALITY_SCORE.md | 第 12 讲 | 质量评分追踪:A/B/C/D 评级,按产品领域和架构层,带变更历史 | | docs/RELIABILITY.md | 第 11 讲 | 运行信号:bootstrap/verify/health check/黄金旅程/可靠性规则 | | docs/SECURITY.md | OpenAI 模板 | 安全规则:secrets、不可信输入、外部动作、依赖评审 | | docs/PLANS.md | OpenAI 模板 | 执行计划规则:何时创建、放哪、最少包含什么 | | docs/exec-plans/active/ | OpenAI 模板 | 当前计划(每个文件一个计划) | | docs/exec-plans/completed/ | OpenAI 模板 | 已完成计划(保留历史上下文) | | docs/exec-plans/tech-debt-tracker.md | 第 12 讲 | 技术债跟踪:延期处理的债务 | | docs/design-docs/ | 第 3 讲 | 设计决策记录(ADR) | | docs/product-specs/ | 第 8 讲 | 用户可见行为规格(验收标准) | | docs/generated/ | OpenAI 模板 | 生成物(db-schema、API 文档),不手改 | ## 四、初始化流程(第 6 讲) 第一个会话只做初始化,不写业务代码。产出五个工件: 1. **可运行的环境** — `make setup` 成功 2. **可验证的测试框架** — 至少一个示例测试通过 3. **启动就绪清单文档** — 写在 `claude-progress.md` 里 4. **任务分解** — `feature_list.json` 至少 3 个功能项 5. **干净的 baseline commit** — git 提交作为检查点 ### 初始化验收清单 ``` ## 初始化验收清单 - [ ] make setup 从零开始能成功 - [ ] make test 至少有一个测试通过 - [ ] 新的 agent 会话能只看仓库回答"怎么跑"和"怎么测" - [ ] feature_list.json 存在且有至少 3 个任务 - [ ] 所有内容已提交到 git ``` ## 五、会话循环(每轮会话的固定流程) 对应第 6 讲的"编码代理开工流程"和第 12 讲的"干净状态": ### 开工流程(每轮会话开始) 1. `pwd` # 确认在正确目录 2. 读 `claude-progress.md` # 恢复持久状态 3. 读 `feature_list.json` # 选择最高优先级未完成功能 4. `git log --oneline -5` # 看最近提交 5. `./init.sh` # 标准化启动 6. 跑基础 smoke test # 确认基线没坏 7. 如果基线已坏,先修基线 # 不在坏状态上叠新功能 8. 选择一个未完成功能,WIP=1 # 只围绕它工作 ### 收尾流程(每轮会话结束) 1. 跑 `make check` # 验证全绿 2. 更新 `claude-progress.md` # 记录进度 3. 更新 `feature_list.json` # 更新功能状态 4. 清理临时工件 # 删 debug 日志、TODO 注释 5. 跑 `clean-state-checklist` # 逐项确认 6. `git commit` # 提交干净状态 7. 必要时写 `session-handoff.md` # 较长会话才需要 ## 六、Loop Engineering 扩展(第 13 讲,可选) 当项目成熟后,可把上述手动流程升级为自动循环: ``` my-project/ ├── .agent/ │ ├── goal-template.md # /goal 命令模板 │ ├── loop-state.md # 循环状态:目标、停止条件、当前迭代 │ ├── maker-prompt.md # 生成者 prompt │ ├── checker-prompt.md # 评估者 prompt(独立于生成者) │ └── automations/ # 自动化触发器 │ ├── timer.md # 定时循环 │ └── file-watch.md # 文件变化触发 ``` ## 七、选择建议 | 项目规模 | 推荐结构 | 理由 | |----------|----------|------| | 原型/POC/小工具 | 最小版,甚至只用 AGENTS.md + init.sh | 第 2 讲:先有反馈子系统,投入产出比最高 | | 中型项目(1-3 万行) | 最小版完整结构 | 第 3-12 讲全部适用 | | 长期维护的大型项目 | 进阶版 | 需要 QUALITY_SCORE.md、PLANS.md、RELIABILITY.md 追踪演化 | | 多 agent 并发项目 | 进阶版 + Loop 扩展 | 需要隔离(第 3 讲 ACID)和自动化循环(第 13 讲) | ## 八、关键注意事项(从课程失败案例中提炼) 1. **不要一上来就用进阶版** — 第 2 讲的团队从 README → AGENTS.md → 加验证命令 → 加进度文件,四次迭代把成功率从 20% 提升到 100%。循序渐进。 2. **AGENTS.md 不要超过 200 行** — 第 4 讲:超过 200 行就开始挤占上下文预算,关键约束会被"中间迷失"。 3. **feature_list.json 是原语,不是备忘录** — 第 8 讲:它必须机器可读、状态由验证门控、agent 不能自己改 passing。 4. **init.sh 必须幂等** — 第 6 讲:无论跑多少次结果一样,失败时重跑也安全。 5. **每条指令标明来源、适用条件、过期条件** — 第 4 讲:像管理代码依赖一样管理指令,定期审计删除过时条目。 6. **干净状态五条件缺一不可** — 第 12 讲:构建通过、测试通过、进度已记录、临时工件已清理、启动路径可用。 7. **知识靠近代码** — 第 3 讲:`src/api/ARCHITECTURE.md` 比根目录一个 500 行的全局文档有用得多。 ---