[{"content":" 这是「LLM 应用开发」系列的第三篇。前两篇结构化输出和 Pydantic 解决的是「单个 agent 怎么说话」，这篇开始讲「agent 本身是怎么搭起来的」。\n为什么需要 LangChain 先看裸调 OpenAI SDK 长什么样：\n1 2 3 4 5 6 7 8 from openai import OpenAI client = OpenAI() resp = client.chat.completions.create( model=\u0026#34;gpt-4o-mini\u0026#34;, messages=[{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;把麻婆豆腐翻译成英文\u0026#34;}], ) print(resp.choices[0].message.content) 能跑，但只要需求稍微复杂一点，痛点就全冒出来：\n换模型要改代码：想从 GPT 换成 DeepSeek、Claude，每个 SDK 接口都不一样，得重写。 Prompt 难管理：f\u0026quot;...\u0026quot; 字符串拼来拼去，变量一多就乱，还没法复用。 输出是裸字符串：想要结构化数据（JSON、对象），得自己写正则解析、自己处理失败重试。 没有组合能力：想把「生成 → 校验 → 重写」串成流程，得自己拿 if/for 硬接。 LangChain 就是来解决这些的——它给 LLM 应用提供了一套标准化的「积木」：统一的模型接口、可复用的提示模板、声明式的流程组合（LCEL）。你不用再手搓这些轮子。\n类比：调原生 SDK 像用 socket 手写 HTTP，用 LangChain 像用 FastAPI——框架帮你把重复劳动抽象掉，你只关心业务逻辑。\n一、LangChain 是什么 一句话定义：\nLangChain 是一个用于构建 LLM 应用的开发框架，核心价值是把「调用 LLM」这件原始的事，抽象成一组可组合、可替换、模型无关的标准组件。\n它解决三件事：\n价值 说明 模型抽象 一套接口（BaseChatModel）适配所有主流 LLM，换模型只改一行 可组合（LCEL） 用 | 把组件像 Unix 管道一样串成流程，声明式而非命令式 生态集成 结构化输出、工具调用、检索（RAG）、回调……都封装好了 本文只讲前两个——因为我的 mako 项目就只用了这两个。第三点（agent 工具、检索）mako 走了另一条路（LangGraph），会在下一篇讲。\n二、主要组件（⚠️ 含版本陷阱） 先说一个非常重要的坑 LangChain 在 2024 年底发布了 1.0 大版本，重构了一批 API，废弃了大量旧接口。问题是：网上绝大多数中文教程（包括很多爆款文章）还在用 0.x 时代的旧 API，照着学全盘皆错。\n我自己一开始总结的组件清单就是从旧教程抄的，后来对照项目代码才发现大半是废弃的。先把这张**「旧 → 新」对照表**摆出来，帮你避坑：\n旧 API（已废弃 / 不推荐） 新版写法 mako 是否用到 langchain.chains.LLMChain LCEL 管道 prompt | llm ✅ langchain.memory.ConversationBufferMemory LangGraph 的 State ❌ langchain.agents.initialize_agent langgraph.create_react_agent ❌ langchain_community.llms.OpenAI（补全模型） langchain_openai.ChatOpenAI（聊天模型） ✅ langchain.embeddings.OpenAIEmbeddings langchain_openai.OpenAIEmbeddings ❌ langchain.document_loaders.TextLoader langchain_community.document_loaders.* ❌ PromptTemplate（纯文本） ChatPromptTemplate（带角色） ✅ PydanticOutputParser .with_structured_output() ✅（用后者） 记住一条判断标准：看到 LLMChain、initialize_agent、ConversationBufferMemory 这三个，基本就是过时教程，直接关掉。\nmako 实际用到的 4 个核心组件 LangChain 组件很多，但 mako 只用了下面 4 个。搞懂这 4 个，就理解了 mako 每个 agent 的内核。\n1. 模型抽象（Chat Models） 所有聊天模型实现同一个接口 BaseChatModel，换 provider 只改实例化代码：\n1 2 3 4 5 6 from langchain_openai import ChatOpenAI # OpenAI / 兼容 OpenAI 的模型 from langchain_anthropic import ChatAnthropic # Claude llm = ChatOpenAI(model=\u0026#34;gpt-4o-mini\u0026#34;, temperature=0) # 想换 Claude？只改这一行： # llm = ChatAnthropic(model=\u0026#34;claude-sonnet-4-6\u0026#34;, temperature=0) 2. 提示模板（Prompts） ChatPromptTemplate 把 prompt 和变量分离，还能区分 system / human 角色：\n1 2 3 4 5 6 7 8 9 10 from langchain_core.prompts import ChatPromptTemplate prompt = ChatPromptTemplate.from_template( \u0026#34;把下面这道中国菜翻译成一句英文介绍：{dish}\u0026#34; ) # 也可以带角色： # prompt = ChatPromptTemplate.from_messages([ # (\u0026#34;system\u0026#34;, \u0026#34;你是翻译助手\u0026#34;), # (\u0026#34;user\u0026#34;, \u0026#34;翻译：{dish}\u0026#34;), # ]) 3. 输出解析 / 结构化输出 LangChain 有两种方式拿结构化数据：\n老办法：StrOutputParser / PydanticOutputParser——模型吐字符串，再用 parser 解析。 新办法（推荐）：.with_structured_output(Schema)——直接让模型返回 Pydantic 对象，底层用 结构化输出那四层技术。 4. LCEL 链（LangChain Expression Language） 这是 LangChain 1.x 的灵魂。 用 | 运算符把上面三个组件串成一条管道，替代了旧的 LLMChain。下一节专门讲。\nmako 没用的组件（简单了解即可） 为完整起见，列一下 LangChain 还有但 mako 没碰的几类——它们不是没用，而是 mako 选了别的方案：\n组件 用途 mako 的替代方案 Agents / Tools 让 LLM 自主调用工具 用 LangGraph 手搓多 agent 图 Memory 多轮对话记忆 用 LangGraph 的 AgentState 共享状态 Retrieval / RAG 向量检索外部知识 用结构化知识模块（IKD），不做向量检索 结论：别试图学完整个 LangChain。它太大，而且很多模块 mako 根本不用。盯住上面 4 个核心组件 + LCEL，就够看懂 mako 了。\n三、LangChain 工作流：固定四步 不管多复杂的 LangChain 应用，核心流程永远是这四步：\n① 创建提示模板 → ② 初始化模型 → ③ 用 | 组装成链 → ④ 调用 invoke 一个通用例子：菜品翻译 需求：输入一道中国菜，输出一句英文介绍。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser # ① 提示模板 prompt = ChatPromptTemplate.from_template( \u0026#34;把下面这道中国菜翻译成一句英文介绍：{dish}\u0026#34; ) # ② 初始化模型 llm = ChatOpenAI(model=\u0026#34;gpt-4o-mini\u0026#34;, temperature=0) # ③ 组装链（LCEL：用 | 把组件串起来） chain = prompt | llm | StrOutputParser() # ④ 调用 print(chain.invoke({\u0026#34;dish\u0026#34;: \u0026#34;麻婆豆腐\u0026#34;})) # -\u0026gt; \u0026#34;Mapo tofu: a spicy Sichuan dish of tofu cooked with chili and minced pork.\u0026#34; 关键：理解 | 管道（LCEL）的数据流 新手最容易卡在 prompt | llm | StrOutputParser() 这行——| 是什么？\n它是运算符重载，行为类似 Unix 管道：把左边组件的输出，喂给右边组件作为输入。整条链的数据流是这样流动的：\ninvoke({\u0026#34;dish\u0026#34;: \u0026#34;麻婆豆腐\u0026#34;}) │ ▼ ┌─────────────────────┐ │ ChatPromptTemplate │ 填充模板，输出 messages └─────────────────────┘ │ messages ▼ ┌─────────────────────┐ │ ChatOpenAI (llm) │ 调用模型，输出 AIMessage └─────────────────────┘ │ AIMessage(content=\u0026#34;Mapo tofu...\u0026#34;) ▼ ┌─────────────────────┐ │ StrOutputParser │ 从 Message 里提取字符串 └─────────────────────┘ │ ▼ \u0026#34;Mapo tofu: ...\u0026#34; 每个组件都是一个「处理单元」，吃一种类型、吐一种类型，| 负责把它们接起来。这就是声明式编程——你描述「流程长什么样」，而不是写「先执行 A，再把结果传给 B」的命令式代码。\n升级版：结构化输出 把第 ③ 步的 parser 换成 .with_structured_output()，就直接拿到结构化对象（衔接前两篇）：\n1 2 3 4 5 6 7 8 9 10 11 from pydantic import BaseModel, Field class DishIntro(BaseModel): en_name: str = Field(description=\u0026#34;英文菜名\u0026#34;) spicy: bool = Field(description=\u0026#34;是否辣\u0026#34;) # 只改这一行：把 StrOutputParser() 换成结构化输出 chain = prompt | llm.with_structured_output(DishIntro) result = chain.invoke({\u0026#34;dish\u0026#34;: \u0026#34;麻婆豆腐\u0026#34;}) print(result.en_name, result.spicy) # Mapo Tofu True 四步还是那四步，只是换了第 ③ 步的一个组件。LCEL 的好处就在这：组件可插拔，流程不变。\n四、结合 mako 项目详解 通用例子看懂了，现在看 mako 真实代码。mako 有 6 个 agent，每个 agent 的内核都是上面那套四步流程。以第一个 agent DataEngineer（数据工程师）为例，它的职责是把原始数据整理成建模可用的数据集：\n1 2 3 4 5 6 7 8 9 10 11 12 # src/mako_langchain/agents/data_engineer.py （简化） def create_data_engineer(provider, model): # ①② 模板和模型 llm = get_llm(provider, model, temperature=0) # 见下文：多 provider 抽象 prompt = get_data_engineer_prompt() # 提示模板（从 prompts 模块加载） # ③ 组装链：LCEL 管道 + 结构化输出 chain = prompt | llm.with_structured_output( DataEngineerOutput, method=\u0026#34;json_mode\u0026#34; ) return chain 发现了吗？这和通用例子的「升级版」是同一个结构：\n通用例子: prompt | llm.with_structured_output(DishIntro) mako: prompt | llm.with_structured_output(DataEngineerOutput) 唯一的区别是 DataEngineerOutput 这个 Schema 更复杂——它装的是「集合、参数、决策变量」这些优化建模元素（详见 Pydantic 那篇）。但骨架完全一样。\n第 ④ 步调用则被包进了一个 LangGraph 节点函数：\n1 2 3 4 5 6 7 def data_engineer_node(state: Dict) -\u0026gt; Dict: chain = create_data_engineer(state[\u0026#34;provider\u0026#34;], state[\u0026#34;model\u0026#34;]) result = chain.invoke({ \u0026#34;role\u0026#34;: DATA_ENGINEER_ROLE, \u0026#34;task\u0026#34;: task, # 由模板里的 {task} 占位符填充 }) return {\u0026#34;data_engineer_output\u0026#34;: result} # 写回共享状态 亮点 1：多 provider 抽象 通用例子里 llm = ChatOpenAI(...) 是写死的，mako 则用 get_llm(provider, model) 动态创建：\n1 2 3 4 5 # src/mako_langchain/utils/llm_config.py （简化） def get_llm(provider=\u0026#34;DeepSeek\u0026#34;, model=None, temperature=0): if provider in ANTHROPIC_PROVIDERS: # MiniMax M2.x 走 Anthropic 端点 return ChatAnthropic(model=model, ...) return ChatOpenAI(model=model, base_url=..., api_key=...) # 其余走 OpenAI 兼容 这就是模型抽象的价值：mako 能在十几个模型（DeepSeek、Qwen、GLM、Kimi、Claude……）之间切换，业务代码（prompt | llm.with_structured_output(...)）一个字都不用改。换模型 = 换 get_llm 的参数。\n亮点 2：为每个模型「打补丁」选结构化输出方法 这是真实项目才会遇到的工程细节。mako 在 llm_config.py 里给不同模型动态生成子类，强制指定结构化输出的 method：\n1 2 3 4 5 # 某些模型用 json_mode 会输出非法 JSON，强制改用 function_calling class PatchedFCChatOpenAI(ChatOpenAI): def with_structured_output(self, schema, method=None, **kw): return super().with_structured_output(schema, method=\u0026#34;function_calling\u0026#34;, **kw) return PatchedFCChatOpenAI(**kwargs) 这正好是结构化输出那篇的实战闭环：博客里讲了「四种 method 怎么选」，mako 在工程上就是为每个模型踩坑后逐个定死 method。理论 → 落地，对上了。\n小结：mako agent 的本质 LangChain chain（内核） LangGraph node（外壳） prompt | llm.with_structured_ ──包装──\u0026gt; def data_engineer_node(state): output(Schema) chain.invoke(...) │ 写回共享状态 LangChain 负责「一个 agent 怎么干活」：prompt + 模型 + 结构化输出，组成一条 LCEL 链。 LangGraph 负责「多个 agent 怎么协作」：把这些链包装成节点，连成一张有状态的图。 下篇预告 这篇讲清了单个 agent 的内核（LangChain chain）。但 mako 是多智能体系统——6 个 agent 要按顺序传数据、出错了要回溯诊断。这些「协作」逻辑，LangChain 本身管不了，是 LangGraph 的活。\n下一篇就讲：怎么用 LangGraph 把这些 chain 节点连成一张会自我纠错的流水线图。敬请期待。\n","permalink":"https://pengkangzhen.github.io/posts/langchain-intro/","summary":"用 mako 多智能体项目的真实代码，讲清 LangChain 的核心组件、LCEL 管道语法，以及网上旧教程里的废弃 API 该怎么替换。","title":"LangChain 入门：从核心组件到 LCEL 管道"},{"content":" 这是「LLM 应用开发」系列的第四篇。上一篇讲了单个 agent 的内核（LangChain 的 LCEL 链），这篇讲怎么把多个 agent 连成一张能协作、能纠错的图。\n上一篇的链，有个硬伤 上一篇我们搭了这样一条 LangChain 链：\n1 2 chain = prompt | llm | StrOutputParser() chain.invoke({\u0026#34;dish\u0026#34;: \u0026#34;麻婆豆腐\u0026#34;}) 它跑一次就结束了。但只要需求稍微真实一点，立刻不够用：\n要分支：翻译完想检查一下，通过了才输出，不通过就重翻——| 管道没法「看结果决定下一步」。 要循环：翻得不好得重来，甚至重试好几次——| 管道是直的，拐不回来。 要共享中间结果：多个步骤要读同一份数据、往里写新字段——管道里数据只能往前流，没地方「存」。 LangGraph 就是来补这三块的。 它给 LangChain 的链加上了「有状态的图」：能分支、能循环、能让多个步骤共享一块「黑板」。\n关系一句话：LangChain 提供 agent 的「内核」（上一篇的 chain），LangGraph 提供「图纸」——把这些 agent 连成一张图。 LangGraph 也是 LangChain 公司出的，依赖 langchain-core，不是竞品，是同一生态的进阶层。\n一、三个核心概念 LangGraph 万变不离其宗，就三样东西：\n1. State（状态）—— 所有节点共享的「黑板」 一个 TypedDict，整张图共享。每个节点能读它，也能往里写新字段。\n1 2 3 4 5 6 from typing import TypedDict class MyState(TypedDict): dish: str # 输入 translation: str # 某个节点产出 approved: bool # 另一个节点产出 关键机制：节点不返回完整 state，而是返回一个部分字典（只含要更新的字段），LangGraph 自动 merge 回去。所以节点之间不用互相传参，大家都读写这块黑板。\n2. Node（节点）—— 干活的函数 一个普通函数，签名固定 def node(state) -\u0026gt; dict：读 state、干活、返回部分更新。\n1 2 3 def translate(state: MyState) -\u0026gt; dict: text = chain.invoke({\u0026#34;dish\u0026#34;: state[\u0026#34;dish\u0026#34;]}) # 用上一篇的 chain return {\u0026#34;translation\u0026#34;: text} # 只返回要更新的字段 节点内部就是上一篇讲的 LangChain 链——LangGraph 不改变 agent 的内核，只是把它包成一个节点。\n3. Edge（边）—— 节点之间的连线 两种边：\n直连 add_edge(A, B)：A 跑完，一定去 B。 条件路由 add_conditional_edges(A, router, mapping)：A 跑完，调用 router(state) 函数，根据返回值决定去哪。这是实现「分支」和「循环」的关键。 1 2 3 4 5 6 7 def decide(state) -\u0026gt; str: return \u0026#34;done\u0026#34; if state[\u0026#34;approved\u0026#34;] else \u0026#34;retry\u0026#34; # 根据状态决定 graph.add_conditional_edges( \u0026#34;review\u0026#34;, decide, {\u0026#34;retry\u0026#34;: \u0026#34;translate\u0026#34;, \u0026#34;done\u0026#34;: END}, # 返回值 → 目标节点 ) 把 \u0026quot;retry\u0026quot; 指回 \u0026quot;translate\u0026quot;，就形成了循环——这是 LCEL 做不到的。\n二、四步搭一张图 不管多复杂的 LangGraph 应用，搭图永远是这四步：\n① 定义 State → ② 定义 Node 函数 → ③ 连边（直连 + 条件路由）→ ④ compile \u0026amp; invoke 通用例子：翻译 + 审校（带循环） 把上一篇的「菜品翻译」升级：翻译完让一个「审校」节点检查，不通过就回去重翻。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 from typing import TypedDict, Literal from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langgraph.graph import StateGraph, END # 复用上一篇的翻译链 llm = ChatOpenAI(model=\u0026#34;gpt-4o-mini\u0026#34;, temperature=0) prompt = ChatPromptTemplate.from_template(\u0026#34;把这道菜翻译成英文：{dish}\u0026#34;) translate_chain = prompt | llm | StrOutputParser() # ① 定义状态 class State(TypedDict): dish: str translation: str approved: bool # ② 定义节点 def translate(state: State) -\u0026gt; dict: text = translate_chain.invoke({\u0026#34;dish\u0026#34;: state[\u0026#34;dish\u0026#34;]}) return {\u0026#34;translation\u0026#34;: text} def review(state: State) -\u0026gt; dict: # demo 用简单规则；真实场景这里换成 LLM 审校（mako 就是这么做的） ok = len(state[\u0026#34;translation\u0026#34;]) \u0026gt; 5 return {\u0026#34;approved\u0026#34;: ok} def decide(state: State) -\u0026gt; Literal[\u0026#34;retry\u0026#34;, \u0026#34;done\u0026#34;]: return \u0026#34;done\u0026#34; if state[\u0026#34;approved\u0026#34;] else \u0026#34;retry\u0026#34; # ③ 连边 g = StateGraph(State) g.add_node(\u0026#34;translate\u0026#34;, translate) g.add_node(\u0026#34;review\u0026#34;, review) g.set_entry_point(\u0026#34;translate\u0026#34;) # 起点 g.add_edge(\u0026#34;translate\u0026#34;, \u0026#34;review\u0026#34;) # 直连 g.add_conditional_edges(\u0026#34;review\u0026#34;, decide, { # 条件路由 \u0026#34;retry\u0026#34;: \u0026#34;translate\u0026#34;, \u0026#34;done\u0026#34;: END, }) # ④ 编译 \u0026amp; 调用 app = g.compile() result = app.invoke({\u0026#34;dish\u0026#34;: \u0026#34;麻婆豆腐\u0026#34;}) print(result[\u0026#34;translation\u0026#34;]) 对应的数据流长这样——注意 review 到 translate 那条回头的箭头，那就是循环：\n┌───────────┐ ┌────\u0026gt;│ translate │ （复用上一篇的 chain） │ └─────┬─────┘ │ ▼ (直连) retry ┌───────────┐ │ │ review │ 审校 └──────┤ │ └─────┬─────┘ decide│ approved? ┌────────┴────────┐ done retry → 回到 translate（循环！） ▼ END 跑起来你会发现：review 不通过时，图会自动回到 translate 重翻，直到通过才结束。这就是 LangGraph 相比 LangChain 链的杀手锏——会循环。\n三、结合 mako 项目详解 通用例子看懂了，现在看 mako 的真实工作流。mako 有 6 个 agent，整张图（src/mako_langchain/graph/workflow.py）就是上面这套模式的工业级版本。\n1. State：mako 的 AgentState mako 的共享状态（graph/state.py）字段更多，但本质就是那张「黑板」：\n1 2 3 4 5 6 7 8 9 10 11 12 13 class AgentState(TypedDict, total=False): # 输入 problem_description: str sample: Dict[str, Any] # 各 agent 的产出（写回黑板） data_engineer_output: Optional[DataEngineerOutput] model_expert_output: Optional[ModelExpertOutput] python_code: Optional[str] execution_result: Optional[Dict[str, Any]] # 错误处理 \u0026amp; 回溯 error_info: Optional[str] retry_count: int # ... 每个 agent 往里写自己的产出（如 data_engineer_output），下游 agent 直接从 state 里读——节点之间从不直接传参。\n2. Node：6 个 agent + 3 个回溯节点 1 2 3 4 5 6 7 # workflow.py workflow.add_node(\u0026#34;data_engineer\u0026#34;, data_engineer_node) # 数据工程师 workflow.add_node(\u0026#34;model_expert\u0026#34;, model_expert_node) # 建模专家 workflow.add_node(\u0026#34;knowledge_loader\u0026#34;, knowledge_loader_node) # 知识加载 workflow.add_node(\u0026#34;python_developer\u0026#34;, python_developer_node) # 写 Gurobi 代码 workflow.add_node(\u0026#34;solver_executor\u0026#34;, solver_executor_node) # 执行求解 workflow.add_node(\u0026#34;diagnosis_agent\u0026#34;, diagnosis_agent_node) # 错误诊断 每个 xxx_node 内部就是上一篇讲的 prompt | llm.with_structured_output(...) 那条链。节点 = 把 LangChain 链包了一层。\n3. Edge：主线流水线 + 条件路由 mako 的主线是一条流水线，中间夹着两个条件路由：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 主线：直连 workflow.add_edge(\u0026#34;data_engineer\u0026#34;, \u0026#34;model_expert\u0026#34;) workflow.add_edge(\u0026#34;python_developer\u0026#34;, \u0026#34;solver_executor\u0026#34;) # 条件路由①：建模后，要不要先加载知识？ workflow.add_conditional_edges(\u0026#34;model_expert\u0026#34;, route_after_model_expert, { \u0026#34;knowledge_loader\u0026#34;: \u0026#34;knowledge_loader\u0026#34;, # 模型请求了新知识 → 先加载 \u0026#34;python_developer\u0026#34;: \u0026#34;python_developer\u0026#34;, # 没请求 → 直接写代码 }) # 条件路由②：求解后，成功还是诊断？ workflow.add_conditional_edges(\u0026#34;solver_executor\u0026#34;, should_diagnose, { \u0026#34;success\u0026#34;: END, # 求解成功 → 结束 \u0026#34;diagnose\u0026#34;: \u0026#34;diagnosis_agent\u0026#34;, # 失败 → 去诊断（进入循环！） }) should_diagnose 的逻辑（workflow.py:45）就是通用例子里 decide 的翻版——看 execution_result 里的 diagnosis_required 标志，决定是收工还是进诊断。\n4. 整张图长这样 data_engineer → model_expert ──┬(无知识请求)──────────────┐ │(请求知识) │ ▼ │ knowledge_loader ──(加载后回到 model_expert) │ │ └────────────────────────────┤ ▼ python_developer │ ▼ solver_executor │ decide ┌───────────────┴───────────────┐ 成功 失败 │ │ END diagnosis_agent │ 指控某 agent ▼ {agent}_backward │ 修正了？ ┌──────────┴──────────┐ 是 否 │ │ 重跑下游 回 diagnosis 换人指控（循环） 对比通用例子：mako 就是「翻译 + 审校」的复杂版——solver_executor 是「审校」，求解失败就进诊断、回到出错的 agent 修正（{agent}_backward），修正了重跑、没修正就换个人指控。整个右侧就是一个会自我纠错的循环。\n5. 关键洞察 回头看，mako 这套架构其实只用到了 LangGraph 最核心的能力：\nLangGraph 能力 mako 怎么用 共享 State AgentState 黑板，agent 间不传参 条件路由 建模后要不要加载知识、求解后成功还是诊断 循环 求解失败 → 诊断 → 回溯 → 重跑，直到成功或耗尽重试 没有黑魔法。把这三样用熟，你也能搭出自己的多 agent 系统。\n下篇预告 这篇讲了图怎么搭、怎么循环。但 mako 最有意思的地方，是循环里到底在干嘛——\n求解失败后，diagnosis_agent 会像「检察官」一样指控最可能出错的那个 agent，被指控的 agent 要自我审查、承认或反驳。这不是普通的「重试」，而是一套对抗式纠错机制。\n下一篇讲：怎么让多个 agent 互相「甩锅」来定位错误。敬请期待。\n","permalink":"https://pengkangzhen.github.io/posts/langgraph-intro/","summary":"LangChain 的链跑完就结束。真实的多 agent 系统需要分支、循环、共享状态——这正是 LangGraph 干的事。用 mako 项目的真实工作流讲清 State、Node、Edge 三件套。","title":"LangGraph 入门：把多个 agent 连成一张会循环的图"},{"content":"为什么 LLM 结构化输出离不开 Pydantic 我在上一篇梳理了结构化输出的四层技术——从 Prompt 工程到约束解码、从 Function Calling 到 Structured Outputs。但你会发现，每一层最终都绕不开同一个东西：一个描述输出\u0026quot;形状\u0026quot;的数据模型，而且几乎都是用 Pydantic 写的。\n旧文讲的是「在哪用 Pydantic」，这篇讲「怎么把 Pydantic 模型写好」。\n为什么偏偏是 Pydantic？因为结构化输出的本质是给 LLM 一份契约：\nPydantic 模型 → model_json_schema() → JSON Schema → 发给 LLM （这就是那座桥梁） （模型据此生成） Schema 即提示词：字段名、类型、description 都会翻译进 JSON Schema，直接决定 LLM 往每个格子里填什么。 校验即兜底：模型生成的 JSON 回来后，Pydantic 再验一遍。不通过就打回重试（旧文第三层的 Instructor / Guardrails 机制）。 这篇就围绕一个真实场景——让 LLM 把一个业务问题翻译成数学优化模型（决策变量、目标函数、约束）——来讲怎么把模型写好。这个场景来自我的多智能体优化项目，足够复杂，能把所有关键技巧都逼出来。\n主要特点与用途 在深入\u0026quot;怎么为 LLM 写模型\u0026quot;之前，先快速了解 Pydantic 的主要能力——结构化输出用到的只是其中一部分：\n数据验证（Data Validation）：数据传入模型时，Pydantic 自动检查字段的类型、长度、范围等是否符合定义；不通过就抛出一个清晰的、详细的错误，告诉你哪里出了问题。 类型转换（Parsing \u0026amp; Serialization）：即使接收到的是\u0026quot;字符串形式的数字\u0026quot;（如 \u0026quot;123\u0026quot;），只要字段定义为 int，就会自动转成 123；反过来也能轻松把模型实例转成字典或 JSON 字符串。 设置管理（Settings Management）：很适合管理应用配置（例如从环境变量或 .env 文件读取），能自动转换类型并提供默认值。 与 IDE 完美配合：因为基于标准类型注解，PyCharm / VSCode 等编辑器能提供出色的自动补全和类型检查支持。 轻量且高性能：核心逻辑用 Rust（pydantic-core）实现，速度非常快。 生态系统的关键角色：它是 FastAPI 等众多顶级工具的核心依赖——FastAPI 就是用 Pydantic 模型自动处理请求/响应的验证、序列化与文档生成；这也是 LangChain 等框架做结构化输出时默认选它的原因。 定义模型：从最简单的例子开始 Pydantic 的核心思路是：你用类型注解定义数据的\u0026quot;形状\u0026quot;，验证、类型转换、序列化全部自动完成。先从一个最简单的模型入门：\n1 2 3 4 5 6 7 8 9 10 11 from pydantic import BaseModel, Field class User(BaseModel): name: str age: int = Field(gt=0) # 年龄必须 \u0026gt; 0 hobbies: list[str] = [] # 默认空列表 # 直接传字典，Pydantic 自动解析 + 验证 user = User(**{\u0026#34;name\u0026#34;: \u0026#34;Alice\u0026#34;, \u0026#34;age\u0026#34;: 30}) print(user.name) # Alice print(user.model_dump()) # {\u0026#39;name\u0026#39;: \u0026#39;Alice\u0026#39;, \u0026#39;age\u0026#39;: 30, \u0026#39;hobbies\u0026#39;: []} 两个要点：\n类型转换（coercion）：哪怕传进来的是字符串 \u0026quot;30\u0026quot;，只要字段声明为 int，Pydantic 也会尝试转成 30。对\u0026quot;不太守规矩\u0026quot;的 LLM 输出尤其有用。 失败即清晰报错：传 age: -5 会抛出 ValidationError，明确告诉你 age 必须大于 0——而不是某个莫名的 AttributeError。 掌握了这套基本套路，接下来进入本文的主线场景：用 Pydantic 为\u0026quot;让 LLM 把业务问题翻译成优化模型\u0026quot;设计输出结构。\nField：约束与描述 Field 是类型注解之外、控制字段行为的主要工具。\n数值约束（常用参数）：\n参数 含义 示例 gt / ge 大于 / 大于等于 ge=0（非负） lt / le 小于 / 小于等于 le=1（不超过 1） multiple_of 倍数 multiple_of=5 description——最容易被忽略、却对 LLM 最重要的参数：\n1 2 3 4 class ObjectiveFunction(BaseModel): direction: str = Field(description=\u0026#34;优化方向，只能是 \u0026#39;min\u0026#39; 或 \u0026#39;max\u0026#39;\u0026#34;) expression: str = Field(description=\u0026#34;目标函数的数学表达式，如 \\\\sum_{i} c_i x_i\u0026#34;) description: str = \u0026#34;\u0026#34; 对普通程序，description 只是文档；但对 LLM 结构化输出，它是提示词的一部分。model_json_schema() 会把 description 原样塞进 schema 发给模型，模型就靠它理解\u0026quot;这个格子到底要填什么\u0026quot;。写好 description，是提升输出质量投入产出比最高的一步。\n为 LLM 写模型的四个进阶技巧 真实的结构化输出几乎不会像 DecisionVariable 这样扁平。下面用优化模型这个场景，逐一演示四个关键技巧。\n1. 嵌套模型：表达复杂结构 一个优化模型由\u0026quot;多个决策变量 + 一个目标函数 + 多条约束\u0026quot;组成，而且每个组件又是结构化的。先把决策变量、约束各拆成独立模型，再嵌套进上层：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class DecisionVariable(BaseModel): symbol: str # 符号，如 x indices: list[str] = [] # 下标，如 [\u0026#34;i\u0026#34;, \u0026#34;j\u0026#34;] type: str # \u0026#34;Continuous\u0026#34; | \u0026#34;Integer\u0026#34; | \u0026#34;Binary\u0026#34; description: str = \u0026#34;\u0026#34; class Constraint(BaseModel): name: str # 约束名（须唯一） expression: str description: str = \u0026#34;\u0026#34; class ModelComponents(BaseModel): decision_variables: list[DecisionVariable] objective_function: ObjectiveFunction # 上一节定义的 ObjectiveFunction constraints: list[Constraint] list[DecisionVariable] 让你表达\u0026quot;任意多条、每条结构一致\u0026quot;的输出，这是扁平字段做不到的。嵌套可以继续往下套，复杂度不受限。\n2. Enum：锁定枚举值 上面的 type 和 direction 是 str + 注释，靠\u0026quot;模型自觉\u0026quot;。但分类值最好是封闭集合，用 Enum 才严谨：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from enum import Enum class VarType(str, Enum): CONTINUOUS = \u0026#34;Continuous\u0026#34; INTEGER = \u0026#34;Integer\u0026#34; BINARY = \u0026#34;Binary\u0026#34; class Direction(str, Enum): MIN = \u0026#34;min\u0026#34; MAX = \u0026#34;max\u0026#34; class DecisionVariable(BaseModel): type: VarType # 只能三选一 class ObjectiveFunction(BaseModel): direction: Direction # 只能 min / max 继承 (str, Enum) 是为了自然序列化成 JSON 字符串。换上 Enum 后，配合约束解码，模型不可能吐出 \u0026quot;linear\u0026quot;、\u0026quot;最大化\u0026quot; 这些 schema 之外的值——它只能在枚举里选。\n顺带一个收益：真实代码原本用 str + 注释，再用验证器手动判断 direction not in {\u0026quot;min\u0026quot;,\u0026quot;max\u0026quot;}。改用 Enum 后，这层手动校验就可以删掉了。\n3. 验证器（after）：类型之外的语义校验 类型和约束只能查\u0026quot;格式\u0026quot;，但 LLM 输出常有\u0026quot;格式对、逻辑错\u0026quot;的问题——比如两条约束重名、变量下标和维度对不上。Pydantic v2 用 @model_validator(mode=\u0026quot;after\u0026quot;) 做跨字段语义校验：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from pydantic import model_validator class ModelComponents(BaseModel): decision_variables: list[DecisionVariable] objective_function: ObjectiveFunction constraints: list[Constraint] @model_validator(mode=\u0026#34;after\u0026#34;) def validate_consistency(self) -\u0026gt; \u0026#34;ModelComponents\u0026#34;: # 约束名必须唯一——抓 LLM 的低级重复 seen: set[str] = set() for c in self.constraints: if c.name in seen: raise ValueError(f\u0026#34;Duplicate constraint name: {c.name}\u0026#34;) seen.add(c.name) return self 这种校验正是 Instructor \u0026ldquo;校验失败 → 把错误拼回 prompt → 重试\u0026quot;循环的驱动力：验证器写得越贴合业务，越能把不靠谱的 LLM 输出挡在门外。（单字段校验用 @field_validator，跨字段用 @model_validator。）\n4. 验证器（before）：容忍 LLM 的\u0026quot;脏\u0026quot;输出 这是对 LLM 场景最实用、却最少被提及的技巧。即便用了结构化输出，模型仍会犯一些\u0026quot;格式性\u0026quot;错误：把本该是对象的字段吐成 JSON 字符串、把列表吐成嵌套列表 [[...]]、在表达式里写 LaTeX 反斜杠把 JSON 弄崩……与其一次次重试，不如在 Pydantic 解析之前先清洗一遍：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import json class ModelComponents(BaseModel): decision_variables: list[DecisionVariable] objective_function: ObjectiveFunction constraints: list[Constraint] @model_validator(mode=\u0026#34;before\u0026#34;) @classmethod def repair_llm_output(cls, data): if not isinstance(data, dict): return data # 字段本该是对象，LLM 却吐成了 JSON 字符串 → 解开它 obj = data.get(\u0026#34;objective_function\u0026#34;) if isinstance(obj, str): data[\u0026#34;objective_function\u0026#34;] = json.loads(obj) # 偶尔吐成嵌套列表：[[\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;]] -\u0026gt; [\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;] cons = data.get(\u0026#34;constraints\u0026#34;) if isinstance(cons, list): data[\u0026#34;constraints\u0026#34;] = [ x for sub in cons for x in (sub if isinstance(sub, list) else [sub]) ] return data mode=\u0026quot;before\u0026quot; 的验证器在 Pydantic 做类型校验之前拿到原始字典，所以能任意改写数据。它和 mode=\u0026quot;after\u0026quot; 搭配，刚好覆盖两类问题：before 修\u0026quot;脏格式\u0026rdquo;，after 查\u0026quot;错逻辑\u0026quot;。\n一个真实的坑：LLM 在表达式里写 \\sum、\\delta、\\text{}，这些反斜杠在 JSON 字符串里是非法转义，会直接让 json.loads 崩掉。处理办法是先把不是合法 JSON 转义的反斜杠翻倍——这通常也是放在 before 验证器或前置清洗函数里做。\n延伸：表达式该用什么表示法？ 上面是\u0026quot;出了问题怎么修\u0026quot;，但其实换个表示法就能从源头绕开。表达式字段让 LLM 输出成什么样，本身就是一个值得权衡的设计决策。用同一个表达式——「对所有 i∈N 累加 c_i × x_i」——对比四种常见表示法：\nLaTeX：`\\sum_{i \\in N} c_i x_i` 渲染后可读性最高，但前面那个反斜杠坑只是冰山一角——\\、{}、^、_ 在 JSON 字符串里全都要双重转义（\\sum 得写成 \\\\sum）。这种\u0026quot;转义爆炸\u0026quot;带来两类故障： 漏一个反斜杠或括号，整段 JSON 就解析失败； 每个转义字符都额外占 token，推理成本上升。\nAMPL-style：`sum {i in N} c[i] * x[i]` 对运筹学者最直观，但 {} 和 JSON 的定界符冲突，需要小心转义；更要命的是它只是一段扁平字符串，无法自动校验符号一致性——程序没法可靠地从中提取引用的变量、再去核对它们是否都声明在决策变量集合里。\nStructured JSON：\n1 2 3 4 {\u0026#34;op\u0026#34;: \u0026#34;sum\u0026#34;, \u0026#34;over\u0026#34;: \u0026#34;i\u0026#34;, \u0026#34;set\u0026#34;: \u0026#34;N\u0026#34;, \u0026#34;body\u0026#34;: {\u0026#34;op\u0026#34;: \u0026#34;*\u0026#34;, \u0026#34;left\u0026#34;: {\u0026#34;op\u0026#34;: \u0026#34;ref\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;c\u0026#34;, \u0026#34;idx\u0026#34;: [\u0026#34;i\u0026#34;]}, \u0026#34;right\u0026#34;: {\u0026#34;op\u0026#34;: \u0026#34;ref\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;x\u0026#34;, \u0026#34;idx\u0026#34;: [\u0026#34;i\u0026#34;]}}} 把表达式拆成算子-操作数树，可校验性最强——能遍历检查每个引用的变量是否合法。代价是 token 开销最高（重复的键名 + 深层嵌套），还显著抬高了结构化输出的 schema 复杂度，反而增加 Pydantic 验证失败的概率。\nS-expression：`(sum i N (* c x))` 前缀括号式，同时避开了上面三者的短处。它只用 () 和符号，不引入任何 JSON 转义字符（\\ {} ^ _ 一个都没有），不会触发解析错误；又是结构化的，可遍历做符号一致性校验；token 开销也远低于 JSON 树。可读性虽不如 LaTeX，但作为\u0026quot;给机器消费\u0026quot;的中间表示，是各项权衡下的甜点。\n表示法 可读性 JSON 友好（无需转义） 可结构化校验 token 开销 LaTeX 高（渲染后） ✗ \\ {} ^ _ 全要转义 ✗ 扁平字符串 高 AMPL-style 高（OR 熟悉） ✗ {} 与定界符冲突 ✗ 扁平字符串 中 Structured JSON 低 ✓ ✓ 最强 最高 S-expression 中 ✓ 只用 () 和符号 ✓ 低 一句话：想要\u0026quot;JSON 安全 + 可校验 + 省 token\u0026quot;，s-expression 是更合适的默认选择；LaTeX 适合最终给人渲染看的场景，但放进结构化输出里就得时刻准备着修复它的转义。本文示例为可读性沿用 LaTeX，实际工程中换成 s-expression 会省心很多。\n完整示例：把模型接进 LLM 把上面的片段拼成一个完整的智能体输出模型，再用 LangChain 接入：\n1 2 3 4 5 6 7 8 from typing import Optional from pydantic import BaseModel, Field, model_validator class ModelExpertOutput(BaseModel): \u0026#34;\u0026#34;\u0026#34;ModelExpert 智能体的结构化输出：一个完整的优化模型定义。\u0026#34;\u0026#34;\u0026#34; model_components: ModelComponents assumptions_made: list[str] = [] # LLM 自行补充的假设 ambiguous_points: Optional[list[str]] = None # 它标记的歧义点 接入只需关键一行——把 Pydantic 模型作为结构化输出的目标：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate llm = ChatOpenAI(model=\u0026#34;deepseek-chat\u0026#34;, temperature=0) prompt = ChatPromptTemplate.from_messages([ (\u0026#34;system\u0026#34;, \u0026#34;{role}\u0026#34;), (\u0026#34;human\u0026#34;, \u0026#34;{task}\u0026#34;), ]) # 关键一行：with_structured_output 让 LLM 直接产出 ModelExpertOutput chain = prompt | llm.with_structured_output(ModelExpertOutput, method=\u0026#34;json_mode\u0026#34;) result: ModelExpertOutput = chain.invoke({ \u0026#34;role\u0026#34;: \u0026#34;你是一名运筹优化建模专家，负责把业务问题描述翻译成 MILP 模型。\u0026#34;, \u0026#34;task\u0026#34;: \u0026#34;请为以下运输问题建立模型：……（问题描述）\u0026#34;, }) # result 已经是验证过的 ModelExpertOutput 实例 print(result.model_components.objective_function.direction) # Direction.MIN print(len(result.model_components.decision_variables), \u0026#34;个决策变量\u0026#34;) 这里有两个真实工程中很值钱的细节：\n结构化输出让智能体之间可以\u0026quot;结构化交接\u0026quot;：下一个智能体的 prompt 里可以直接塞上一个智能体的 model_dump_json(indent=2)，不再是难以解析的自然语言。 约束方法越弱，Pydantic 越要强：这里用的是 method=\u0026quot;json_mode\u0026quot;（旧文里可靠性最弱的一档，但跨 Provider 兼容性最好）。正因为约束弱、模型可能输出\u0026quot;脏 JSON\u0026quot;，前面那套 before 清洗 + after 校验就成了不可或缺的安全网。 Pydantic v1 vs v2 小贴士 本文用的都是 v2 语法。但网上很多教程和存量代码还是 v1，几个最容易踩的差异记一下：\n用途 v1（旧） v2（本文） 转字典 .dict() .model_dump() 转 JSON .json() .model_dump_json() 不可变更新 .copy(update=...) .model_copy(update=...) 跨字段验证器 @root_validator @model_validator 从 JSON 解析 .parse_raw() .model_validate_json() v2 核心用 Rust（pydantic-core）重写，速度比 v1 快一个量级，新项目直接用 v2 即可。\n小结 结构化输出的四层技术再花哨，落点都是那个 Pydantic 模型。把它写好，输出就稳了一大半：\ndescription 是提示词——模型靠它理解每个格子填什么，写清楚。 嵌套 + list 表达复杂结构，Enum 锁定分类输出。 @model_validator(mode=\u0026quot;after\u0026quot;) 做语义校验，抓 LLM 的逻辑错误并驱动重试。 @model_validator(mode=\u0026quot;before\u0026quot;) 清洗 LLM 的脏格式——before 修格式、after 查逻辑，两者搭配。 model_json_schema() 是连接 Pydantic 与 LLM 的那座桥。 想了解这些模型最终如何被四种技术\u0026quot;喂\u0026quot;给 LLM，回到《LLM 结构化输出技术：从原理到实践》。\n参考资源 Pydantic v2 官方文档 LangChain with_structured_output OpenAI Structured Outputs ","permalink":"https://pengkangzhen.github.io/posts/pydantic-structured-output/","summary":"结构化输出的可靠性最终都落在那个 Pydantic 模型上。本文以一个\u0026rsquo;让 LLM 建立运筹优化模型\u0026rsquo;的真实场景，从基础定义讲到嵌套、Enum、before/after 验证器，再到 LangChain 接入。","title":"Pydantic 入门：为 LLM 结构化输出写好数据模型"},{"content":"引言 在运筹学和优化领域，将现实世界的业务问题转化为数学模型一直是一个高度专业化的任务。尽管优化算法（如 MILP）在过去几十年取得了巨大进展，但建模过程仍然严重依赖运筹学专家——据统计，81% 的 Gurobi 商业求解器用户拥有高等学位，其中 49% 拥有运筹学学位。这种专业知识壁垒导致许多中小型企业、非政府组织和地方机构无法充分利用优化技术改善运营。\n本文解读的 OptiMUS-0.3 论文，提出了一种基于大语言模型的模块化智能体系统，旨在打破这一壁垒。该系统通过创新的\u0026quot;连接图\u0026quot;机制处理长文本、多重纠错机制抑制幻觉、以及结构检测代理优化求解效率，实现了从自然语言描述到高效求解代码的端到端自动化。这篇论文发表于 ICML 2024（机器学习国际顶级会议，CCF-A），为 LLM 在复杂工程问题中的应用提供了极具价值的参考。\n以下是原文内容：\nOptiMUS-0.3: Using Large Language Models to Model and Solve Optimization Problems at Scale\n论文第二版发表于International Conference on Machine Learning（简称 ICML，国际机器学习大会）是机器学习与人工智能领域的国际顶级学术会议，代表了该领域的最高学术水平。中国计算机学会（CCF）最高级别（A类）\n论文叙事 这篇论文主要致力于解决优化建模（Optimization Modeling）中的\u0026quot;专业知识壁垒\u0026quot;问题，以及现有大语言模型（LLM）在自动化求解复杂优化问题时面临的四大技术瓶颈。\n具体来说，论文解决了以下几个层面的问题：\n1. 核心业务痛点：优化建模的\u0026quot;专业知识差距\u0026quot; (Expertise Gap) 问题现状：尽管优化算法（如混合整数线性规划 MILP）在过去几十年取得了巨大进展，但将现实世界的业务问题转化为数学优化模型，仍然高度依赖运筹学专家的专业知识（例如，81%的 Gurobi 商业求解器用户拥有高等学位，其中 49% 拥有运筹学学位）。 造成的后果：这种高昂的专业门槛阻止了许多组织（如小型企业、非政府组织、地方医院或超市）使用优化技术来改善运营（如库存管理、排班、能源管理），导致许多问题仍靠人工经验启发式解决，而非最优求解。 论文目标：通过自动化优化建模，降低使用门槛，让没有运筹学背景的利益相关者也能制定和解决复杂的决策问题，或至少大幅提高现有优化专家的生产力（类似于 GitHub Copilot 对软件工程师的作用）。 2. 现有技术瓶颈：现有 LLM 在处理优化问题时的四大缺陷 论文指出，虽然 LLM 有潜力自动化这一过程，但直接应用现有 LLM 会面临四个主要挑战，而该论文的方法正是为了克服这些挑战：\n长问题描述 (Long problem descriptions)：现实世界的优化问题文档可能长达数十页。LLM 的上下文窗口有限，且随着输入长度增加，其推理和建模性能会显著下降。 大规模问题数据 (Large problem data)：优化问题通常包含大量数值数据（如客户属性、销售数据）。直接将数值数据喂给 LLM 并使用简单公式的方法，只能处理最简单的\u0026quot;玩具问题\u0026quot;，无法应对工业级规模。 幻觉 (Hallucination)：LLM 可能会生成看似合理但数学上错误的约束，或者捏造不存在的求解器 API 调用，导致生成的代码无法运行。更棘手的是，即使代码能运行，也很难验证其逻辑是否正确（例如，漏掉了一个关键约束可能导致求解器返回无界解）。 糟糕的模型效率 (Bad models)：优化问题的求解时间高度依赖于建模公式的选择以及如何向求解器传达问题的结构。LLM 很难不仅生成\u0026quot;正确\u0026quot;的模型，还能生成像专家那样\u0026quot;高效\u0026quot;的代码（例如利用特殊有序集 SOS 或指示变量等高级结构）。 3. 论文提供的直接解决方案 为了解决上述问题，论文提出并开发了 OptiMUS-0.3，一个基于 LLM 的模块化智能体系统。它解决了：\n端到端自动化：能够直接从自然语言描述中，自动提取参数、构建数学模型（LaTeX）、生成并调试求解器代码（如 Gurobi Python API），最终输出最优解。 长文本与大数据处理：通过引入\u0026quot;连接图（Connection Graph）\u0026ldquo;和模块化流水线，系统可以独立处理每个约束和目标，无需将所有信息塞入一个超长 Prompt 中，从而突破了 LLM 的上下文限制。 高可靠性与防幻觉：引入了反思性提示（Reflective prompts）、基于置信度的反馈（Confidence-based feedback）以及自动代码调试（Debug Code）等多重纠错机制，大幅降低了建模错误率。 求解效率优化：通过\u0026quot;结构检测代理（Structure Detection Agent）\u0026ldquo;和\u0026quot;高级优化编码代理（Advanced Optimization Coding Agent）\u0026quot;，系统能识别并利用高级求解器特性（如 SOS、延迟约束生成），生成不仅正确而且求解速度更快的代码。 总结：该论文从根本上试图打破运筹学建模的专业壁垒，通过工程化、模块化的 LLM Agent 架构，克服了 LLM 在长文本、幻觉和代码生成上的固有缺陷，实现了从自然语言到高效、准确的大规模优化模型及求解代码的端到端自动化。\n多智能体协作系统设计 系统提示词 OptiMUS-0.3 中的每一个 LLM 组件都由一个自然语言指令（Prompt）控制，并且每个 Prompt 都严格包含以下三个关键要素：\n1. 任务描述 (Task Description) 内容：明确告诉 LLM 当前需要执行的具体任务。 示例：例如提示 LLM\u0026quot;你的任务是从这段定义优化问题的文本中提取自然语言约束\u0026rdquo;。 2. 问题上下文 (Problem Context) 内容：向 LLM 提供与当前问题和系统当前进度相关的背景信息。 动态加载机制： 在公式化（建模）阶段：上下文包含当前正在处理的具体子句（约束或目标），以及系统到目前为止已经定义的所有参数和变量。 在编码阶段：上下文包含该子句及其数学公式，并且利用前文提到的\u0026quot;连接图（Connection Graph）\u0026ldquo;动态加载与该子句直接相关的参数和变量。这确保了 LLM 不会被无关的冗余信息干扰，精准聚焦于当前代码片段所需的上下文。 3. 示例 (Examples) 内容：在 Prompt 中提供一组固定的任务样本输出。 作用：利用上下文学习（In-Context Learning, ICL） 技术，帮助 LLM 更好地理解任务格式和逻辑。 特定 API 支持：对于需要输出 Python 代码的模块，系统还会专门提供详细演示 gurobipy（Gurobi 求解器的 Python 接口）API 用法的示例，以减少 LLM 产生\u0026quot;幻觉 API\u0026quot;的概率。 多重纠错机制 (Error Correction, EC) 反思提示 (Reflective Prompts) 针对运筹学建模中常见的错误设计特定的反思问题。例如，让 LLM 检查\u0026quot;约束等号两边的单位是否一致？\u0026ldquo;或\u0026quot;该值是已知的参数还是未知的变量？\u0026quot;，从而让 LLM 自我发现并纠正建模错误。以图3为例： 另外一个例子是Figure10：\n基于置信度的反馈 (Confidence-based Feedback) 要求 LLM 对其生成的约束或代码进行 1-5 分的置信度评估。如果置信度低于 5 分，系统会触发求助机制，将问题交由人类用户（通过 Web 界面）或更强大的 LLM（如 Llama 3 调用 GPT-4o）来审核和修正。\n代码调试 (Debug Code) 将代码运行时的报错信息反馈给 LLM，让其自动修改代码，最多迭代 5 次直至运行成功。\nStructure Detection Agent（结构检测代理） 论文中的 4.4. Structure Detection Agent（结构检测代理） 这一小节，主要介绍了一个专门用于识别并利用优化问题中\u0026quot;特殊数学结构\u0026quot;的 LLM 模块，其核心目的是大幅提升求解器的计算效率。\n具体来说，该小节涵盖了以下几个核心要点：\n1. 动机：为什么需要检测结构？ 现代高级优化求解器（如 Gurobi、CPLEX）在遇到特定的数学结构时，求解速度会显著加快。优化专家通常会根据问题结构选择特定的建模方式。然而，让 LLM 直接生成这些高级结构很有挑战性。论文指出，在 NLP4LP 数据集中，约有 10% 的问题包含这些特殊结构（在实际工业应用中比例可能更高）。\n2. 核心机制：如何检测并利用结构？ OptiMUS 维护了一个\u0026quot;优化结构池\u0026rdquo;（包含如 特殊有序集 SOS、指示变量 Indicator variables、半连续变量、分段线性约束等）。\n迭代检测：对于每一个约束或变量，系统会生成一个专门的\u0026quot;结构检测提示（Prompt）\u0026quot;，其中包含该结构的定义和示例。 LLM 判断与重写：LLM 会判断当前公式是否适用该结构。如果适用，系统会调整数学公式以突出该结构（例如，将\u0026quot;一组变量中最多只能有一个非零\u0026quot;的约束，重写为 Type-1 SOS 约束）。 调用高级 API：调整后的结构会通过求解器的高级 Python 接口（如 gurobipy 的 SOS 或 Indicator 接口）直接传递给求解器。 3. 这样做带来的两大优势： 加速分支定界：求解器可以利用这些原生结构来制定更高效的分支规则（Branching rules）。 生成更紧的松弛模型：即使求解器在底层将这些高级结构重新转化为线性约束（例如为 Indicator constraints 使用 Big-M 法），求解器内部的自动化方法通常也能比 LLM 直接生成的 Big-M 值选择得更好、更紧凑，从而提升求解性能。 4. 扩展：问题级别的结构检测 (Problem Structure Pool) 除了约束/变量级别的结构，该代理还维护了一个 \u0026ldquo;问题结构池\u0026rdquo;，用于识别整个问题的组合优化类型（如旅行商问题 TSP、网络流、路由问题等）。如果识别出这类问题，OptiMUS 会在 Web 应用中提示用户：建议使用专用的定制求解器（例如用 Concorde 求解 TSP，而不是用通用的 MILP 求解器），以获得极高的求解效率。\n5. 运行阶段与实证效果 运行时机：该代理在流水线的 \u0026ldquo;公式化子句 (Formulate Clauses)\u0026rdquo; 阶段运行。 实证支持：论文通过 Figure 5 展示了实际案例，证明在设施选址问题中识别出 Indicator variables，或在航班分配问题中识别出 SOS 约束后，相比于朴素的建模实现（Naive implementation），求解时间得到了显著的缩短（Speedup）。 总结：这一小节展示了 OptiMUS 不仅仅是在做\u0026quot;自然语言到代码\u0026quot;的简单翻译，而是通过引入领域知识（运筹学结构），让 LLM 学会像人类专家一样\u0026quot;雕琢\u0026quot;数学模型，从而在保证正确性的同时，极大优化了求解器的运行效率。\nAdvanced Optimization Coding Agent 对于极大规模的优化问题，传统的直接求解往往效率低下。在实际应用中，优化求解器通常被嵌入到高级优化框架中作为子程序调用（例如：列生成 Column Generation、Benders 分解、割平面法等）。为了在这种大规模场景下利用变量和约束的结构，代码必须调用高级求解器接口（例如回调函数 callbacks、模型属性查询与分析）。\n高级功能利用：该代理能够生成利用高级求解器功能（如 callbacks）的代码。 迭代筛选（Sifting）：它可以在简单的变量筛选或约束筛选（Constraint Sifting，也称为延迟约束生成 Delayed Constraint Generation） 方案中，迭代地调用求解器。 无需显式重构：这些模块的优势在于，它们不需要让 LLM 显式地重新表述整个问题（例如在列生成中手动推导并生成定价问题），而是通过高级接口直接提升计算性能，优于朴素的代码实现（Naive implementation）。 工作方式：与结构检测代理类似，该代理维护了一系列针对这些高级功能的模板提示（Template Prompts）。在提示中，LLM 会被告知该模板的目的，并据此生成相应的 Python 代码。 Experiments 实验整体思路非常具有借鉴意义，通过 Overall Performance 证明了\u0026quot;系统比别人强\u0026rdquo;，通过 Ablation Study 证明了\u0026quot;各个模块都有用\u0026rdquo;，还通过上述补充内容深入剖析了 \u0026ldquo;系统跑得有多快\u0026rdquo;、\u0026ldquo;系统有多稳定\u0026rdquo;、\u0026ldquo;系统的自我评估准不准\u0026rdquo; 以及 \u0026ldquo;系统到底会在哪里犯错\u0026rdquo;。这使得整篇论文的实验部分非常严密、饱满且具有工程指导价值。\n首先是Overall performance在不同的数据集上对比基线方法，证明文章所提出的框架的有效性 然后是消融实验,通过逐一移除框架中的关键纠错模块（如代码调试、提取/建模阶段的自我反思纠错、LLM置信度反馈），观察模型准确率的下降幅度，从而量化证明了每个组件对提升整体性能的独立贡献。 除了整体性能对比（Overall Performance）和消融实验（Ablation Study）之外，论文的第 5 章（Experiments）还包含了非常详实的系统特性分析和错误归因分析。\n1. 计算时间分析 (On Computation Time) 对比人类专家：论文统计了 OptiMUS-0.3 在 NLP4LP 数据集上 85 个随机实例的运行时间。系统完成建模和求解的中位数时间仅为 108 秒（最长不超过 350 秒）。相比之下，人类领域专家完成类似任务平均需要约 150 分钟。 瓶颈定位：通过分析各阶段的耗时分布（Figure 7），发现代码生成（Coding）和调试（Debugging） 是整个流水线中最耗时的部分。论文也指出，未来引入专门针对代码生成的小型 LLM 代理可以进一步缩短时间。 2. 随机性与鲁棒性分析 (On Stochasticity in LLM Outputs) 验证稳定性：由于 LLM 的输出具有概率性（随机性），论文特意选取了 10 个困难（Hard）实例，使用 5 个不同的随机种子重复运行了 OptiMUS-0.3。 结论：在所有实例中，系统的最终结果（成功或失败）在 5 次运行中保持了 100% 的一致性。这证明 OptiMUS 框架（特别是其纠错模块 EC）能够有效抑制 LLM 底层随机性带来的性能波动，具有很强的鲁棒性。 3. 置信度校准分析 (On Calibration) 验证\u0026quot;求助机制\u0026quot;的有效性：为了验证 4.3.2 节中提到的\u0026quot;基于置信度的反馈\u0026quot;是否靠谱，论文随机抽取了 40 个约束建模结果，并让运筹学博士生进行人工标注。 结论：在 LLM 给出满分（5/5）置信度的 28 个样本中，正确率达到了 100%；而在置信度低于 5 分的 12 个样本中，有 91.7% 确实存在错误。这证明了 LLM 的自我置信度评估能够有效引导系统或人类用户去拦截和修正潜在的错误。 失败案例与错误归因分析 (Failure Cases) 论文对系统未能成功求解的案例进行了人工定性分析（基于扎根理论），将失败原因归纳为三大类，并对比了简单与困难数据集的错误分布差异（Figure 8 right）：\n三大错误类型： 提取错误 (Extraction errors)：提取了错误的约束/目标，或漏掉了某些约束。 公式化/建模错误 (Formulation errors)：数学公式与自然语言描述不符（如用错了参数来界定变量边界）。 编码错误 (Coding errors)：即使经过调试，代码仍报错（如数组索引越界）。 难度差异分析： 在简单数据集（Easy） 上，系统几乎能提取所有正确的子句，失败主要发生在最后的编码阶段（Coding errors）。 在困难数据集（Hard，包含 MILP 和多维变量） 上，参数提取和数学建模的难度远大于代码编写，绝大多数失败都源于早期的提取和建模错误。 评价指标 作者放弃了文献中常用的\u0026quot;编译错误率（CE rate）\u0026ldquo;和\u0026quot;运行错误率（RE rate）\u0026quot;，而是仅采用\u0026quot;准确率（Accuracy）\u0026ldquo;作为唯一的核心评价指标。\n作者指出，如果只看代码是否能跑通，模型可能会\u0026quot;作弊\u0026rdquo;（例如生成一段无关但能运行的短代码，或者为了消除报错直接把关键约束代码删掉）。因此，论文对\u0026quot;准确率\u0026quot;的定义非常严苛。\n具体来说，一个优化问题实例被判定为\u0026quot;正确求解（即计入准确率）\u0026quot;，必须同时满足以下三个条件：\n1. 代码成功运行 (Code runs successfully) 生成的 Python/Gurobi 代码必须能够顺利执行，不能出现任何编译错误（Compilation Error）或运行时错误（Runtime Error）。\n2. 最优目标值正确 (Optimal value is correct) 代码求解得出的目标函数最优值（Optimal value），必须与数据集中提供的标准答案（或人工求解得出的答案）完全一致。\n3. 最优解正确 (Optimal solution is correct) 代码求出的具体决策变量赋值（即最优解），必须与标准答案一致。\n细节补充（LLM 辅助匹配）：由于 LLM 生成的代码在输出解的格式、变量命名上可能与数据集里的标准答案不完全一样，论文专门调用了一个 LLM 来判断 OptiMUS 生成的解与标准答案在数学和逻辑上是否等价。 ","permalink":"https://pengkangzhen.github.io/posts/optimus-0.3-paper-notes/","summary":"深入解析 OptiMUS-0.3 如何通过模块化 LLM Agent 架构，克服长文本、幻觉和代码生成缺陷，实现从自然语言到高效优化模型的端到端自动化。","title":"OptiMUS-0.3: 基于 LLM 的大规模优化问题自动求解"},{"content":"背景 用过 Claude Code 的开发者大概都遇到过这样的场景：一个终端在跑测试，你只能干等着；一个模块在重构，另一个模块的 bug 修复只能排队。串行工作流浪费了大量等待时间。\nClaude Code 提供了多层级的并行能力来解决这个问题。本文从基础到进阶，依次介绍 Git Worktree 隔离并行、子代理（Subagent）并行 和 Agent Teams 多代理协作，帮你根据场景选择最合适的并行模式。\n第一层：Git Worktree 隔离并行 最朴素的方式：多终端多实例 最直观的做法是开多个终端窗口，每个终端跑一个 claude 会话。但问题很快出现：多个会话操作同一个工作目录，文件互相覆盖，Git 分支混乱，冲突不断。\n核心矛盾在于没有隔离。\nGit Worktree：隔离的并行 Git Worktree 让同一个仓库拥有多个独立的工作目录，每个目录对应一个独立分支。Claude Code 原生支持这个能力，只需一个参数：\n1 2 3 4 5 # 终端 1：创建 feature-auth 分支的 worktree 并启动 Claude claude --worktree feature-auth # 终端 2：创建 bugfix-123 分支的 worktree 并启动 Claude claude --worktree bugfix-123 也可以简写为 claude -w：\n1 claude -w feature-auth 这样做的好处：\n自动创建分支：Worktree 会基于默认分支（origin/HEAD）自动创建一个新分支 文件完全隔离：两个 Claude 会话可以同时修改同名文件，互不影响 合并时再解决冲突：每个 Worktree 是独立的工作目录 + 独立的分支，最终通过 git merge 合并，只在合并时解决冲突 默认情况下，Worktree 会创建在仓库根目录下的 .claude/worktrees/\u0026lt;name\u0026gt;/ 中。\n实际工作流 假设你有两个独立任务：重构认证模块和修复支付 bug。\n1 2 3 4 5 6 7 8 9 10 # 终端 1 claude -w refactor-auth # → 在新 worktree 中启动 Claude，开始重构 # 终端 2 claude -w fix-payment-bug # → 在另一个 worktree 中启动 Claude，修复 bug # 两个 Claude 同时工作，互不干扰 # 完成后各自提交，然后合并回主分支 配置技巧 携带环境文件：Worktree 是全新的检出，未跟踪的文件（如 .env）不会自动复制。在项目根目录创建 .worktreeinclude 文件来指定需要复制的内容（语法与 .gitignore 相同）：\n.env .env.local config/secrets.json 选择基础分支：默认从远程默认分支创建。如果想从当前 HEAD（包含未推送的提交）创建，在设置中配置：\n1 2 3 4 5 { \u0026#34;worktree\u0026#34;: { \u0026#34;baseRef\u0026#34;: \u0026#34;head\u0026#34; } } 从 PR 创建：可以直接基于某个 Pull Request 创建 Worktree 来 review 或修改：\n1 claude --worktree \u0026#34;#1234\u0026#34; 第二层：子代理（Subagent）并行 Worktree 解决了文件隔离的问题，但你仍然需要手动开多个终端、管理多个会话。子代理（Subagent）更进一步——不需要打开新终端，所有并行任务都在当前会话内完成。\n子代理在独立的上下文中执行辅助任务，完成后将结果摘要返回主会话。你不需要关心它的调度过程。\n子代理的几种模式 Claude Code 会根据任务类型自动选择合适的子代理模式，你也可以显式指定：\n模式 说明 适用场景 Explore 文件/代码探索，搜索项目结构 查找 API 端点、理解代码架构 Plan 制定实施计划 复杂功能的拆分与规划 General 通用多步骤任务执行 编写代码、运行命令、修改文件 例如，你可以在会话中说「搜索项目中所有的 API 端点」，Claude 会自动启动一个 Explore 类型的子代理来执行。\n前台 vs 后台运行 子代理有两种运行方式：\n前台运行（默认）：\n阻塞主会话，子代理完成后才能继续 所有权限操作需要你手动审批 适合需要你监督的任务 后台运行：\n子代理在后台执行，完成后通知你结果 不阻塞主会话，你可以继续做其他事 启动时预审批权限，之后自动执行 安全建议：后台任务一般只给只读权限 后台运行尤其适合这些场景：\n搜索和汇总大量代码 分析项目依赖关系 读取日志文件 子代理 + Worktree 组合 当子代理需要编辑文件时，可以让它在独立的 Worktree 中运行，避免与主会话冲突：\n临时方式：要求 Claude「在 worktree 中运行子代理」 永久方式：在自定义子代理的 frontmatter 中添加 isolation: worktree 每个子代理获得一个临时 Worktree，子代理完成且没有更改时会自动删除。\n三种子代理编排模式 掌握了基本的子代理用法后，以下是三种常见的编排模式：\n1. 并行探索 + 汇总 让多个子代理同时从不同角度探索代码库，最后汇总结果。\n你：帮我分析这个项目的架构，分别从路由、数据层、测试覆盖三个角度探索 Claude 会启动多个 Explore 子代理并行工作，各自探索不同维度，最终在主会话中汇总成一份架构分析报告。\n2. 链式闭环：发现 → 修复 → 验证 子代理之间形成自动化链路：一个负责发现问题，另一个负责修复，第三个负责验证。\n你：跑一遍测试套件，失败的测试自动修复，然后重新验证 这适合处理重复性的修复工作，Claude 会自动闭环处理。\n3. 隔离测试运行 让子代理跑测试套件，大量输出不会污染主会话的上下文，只报告失败和摘要结果。\n你：在后台跑完整测试套件，只报告失败用例和总结 这种方式特别适合测试输出量大的项目，避免上下文浪费。\n第三层：Agent Teams 多代理协作 子代理适合「主代理派活、干完汇报」的场景。但如果任务更复杂——成员之间需要讨论、互相挑战、协调接口——就需要 Agent Teams。\nAgent Teams 的核心区别：子代理是星型拓扑（所有结果汇总到主代理），而 Agent Teams 是网状拓扑——团队成员之间可以直接通信、共享任务列表、自主认领任务。\n架构：四种核心组件 一个 Agent Team 由以下部分构成：\n组件 角色 Team Lead（团队负责人） 主会话，负责任务拆解、成员管理、结果综合 Teammates（团队成员） 独立的 Claude Code 实例，各自拥有独立的上下文窗口 Task List（共享任务板） 类似 Jira/Trello，支持依赖关系和自动解锁 Messaging（消息系统） 成员之间直接通信，不一定要经过 Lead 中转 一个类比：子代理像「经理给几个助理派活，助理只向经理汇报」；Agent Teams 像「项目经理带一支真正的开发团队，成员之间可以互相讨论和协作」。\n启用 Agent Teams Agent Teams 目前是实验性功能，默认关闭。启用方式：\n在 ~/.claude/settings.json 中添加：\n1 2 3 4 5 { \u0026#34;env\u0026#34;: { \u0026#34;CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS\u0026#34;: \u0026#34;1\u0026#34; } } 修改后需要重启 Claude Code。\n创建第一个团队 启用后，直接用自然语言描述任务和团队结构即可：\n我要重构这个项目的认证模块。创建一个团队： - 一个成员负责分析现有代码的安全问题 - 一个成员负责设计新的认证方案 - 一个成员负责编写测试用例 让它们先各自调研，然后讨论确定最终方案。 Claude 会自动创建团队、分配任务、协调工作。\n两种显示模式 模式 说明 要求 In-process 所有成员在主终端内运行，用 Shift+Down 切换 无额外要求 Split panes 每个成员一个独立面板，同时可见 需要 tmux 或 iTerm2 默认是 auto：如果在 tmux 会话中则自动分屏，否则 in-process。可以在 settings.json 中配置：\n1 2 3 { \u0026#34;teammateMode\u0026#34;: \u0026#34;tmux\u0026#34; } 或通过命令行指定：\n1 claude --teammate-mode in-process 分屏效果示意：\n┌─────────────────┬─────────────────┬─────────────────┐ │ Teammate 1 │ Teammate 2 │ Teammate 3 │ │ 分析安全问题... │ 设计认证方案... │ 编写测试用例... │ │ │ │ │ │ \u0026#34;@Teammate 2, │ │ │ │ JWT 的过期 │ │ │ │ 策略你定了吗？\u0026#34;│ │ │ └─────────────────┴─────────────────┴─────────────────┘ 指定成员数量和模型 Claude 会根据任务自动决定成员数量，你也可以显式指定：\n创建一个 4 人团队来并行重构这些模块。每个成员使用 Sonnet 模型。 推荐配置：Team Lead 用 Opus（需要强大推理来协调），Teammates 用 Sonnet（性价比高）。\n任务管理与成员交互 共享任务板：Lead 创建任务，成员可以认领（Claim）或被分配。任务支持依赖关系——只有前置任务完成后，后续任务才会解锁。\n直接交互：你可以和任何成员直接对话，不需要经过 Lead：\nIn-process 模式：Shift+Down 切换成员，直接输入消息 分屏模式：点击对应面板直接操作 质量门控：可以要求成员在实施前先提交计划，经 Lead 审批后再动手：\n创建一个架构师成员来重构认证模块。 要求它在修改代码前先提交计划，审批通过后才能开始实施。 子代理 vs Agent Teams：如何选择 对比维度 子代理（Subagent） Agent Teams 通信方式 只向主代理汇报 成员之间可直接通信 协调机制 主代理统一管理 共享任务板 + 自主认领 上下文 独立上下文，结果返回主会话 每个成员完全独立的上下文 Token 成本 较低 较高（每个成员独立消耗） 适合场景 快速、明确的单一任务 需要讨论、协作的复杂任务 简单记：\n子代理：任务分发工具，「干完活汇报」 Agent Teams：真正的协作团队，「成员之间可以讨论、辩论、协同」 Agent Teams 最佳实践 1. 合同优先：在成员并行工作前，先定义好接口契约（函数签名、数据格式、API 规范），避免各做各的对不上。\n2. 控制团队规模：建议 3-5 人起步。每个成员负责 5-6 个中等粒度的任务（15-30 分钟可独立完成的单元）。\n3. 避免文件冲突：让不同成员负责不同的文件/目录。如果必须改同一文件，设计串行修改阶段。\n4. 提供充分上下文：成员启动时不知道之前的对话历史，在 prompt 中交代清楚项目背景、技术栈、文件结构。\n5. 先研究再实现：先让成员做调研和方案设计，讨论确定后再动手写代码，避免写到一半发现方案行不通。\n实战场景：竞争性调试 这是 Agent Teams 最亮眼的使用模式之一——当 bug 的根因不明确时，让多个成员从不同假设出发并行调查：\n用户报告应用在发送一条消息后就退出了，而不是保持连接。 创建 5 个成员来调查不同的假设。让它们互相质疑对方的结论， 像科学辩论一样。把最终的共识更新到调查文档中。 这种「竞争性假设」模式的优势：单个 AI 容易锚定在第一个看起来合理的解释上，而多个独立调查者互相挑战，能更快收敛到真正的根因。\n第四层：Headless 模式与脚本编排 Headless 模式（claude -p）本身不是并行模式，而是非交互执行模式。但它是批量并行的基础设施——你可以用 Shell 脚本编排多个 Headless 会话实现自定义的批量并行。\n什么是 Headless 模式 1 2 3 4 5 6 7 8 # 非交互式执行，结果直接输出到 stdout claude -p \u0026#34;Find and fix the bug in auth.py\u0026#34; --allowedTools \u0026#34;Read,Edit,Bash\u0026#34; # 管道输入输出 cat build-error.txt | claude -p \u0026#39;explain this build error\u0026#39; \u0026gt; output.txt # 获取结构化 JSON 输出 claude -p \u0026#34;Summarize this project\u0026#34; --output-format json 核心特征：\n无交互界面——一条命令出结果，不需要终端对话 可脚本化——适合集成到 CI/CD、Shell 脚本、Python/TypeScript 程序中 单次执行——每次 claude -p 跑一个任务 用脚本编排批量并行 视频里说的\u0026quot;Headless 批量并行\u0026quot;，指的是用 Shell 脚本 + claude -p + Worktree 组合实现：\n1 2 3 4 5 6 7 8 9 # 在 Shell 脚本中并行启动多个 Headless 会话 claude -w task-1 -p \u0026#34;实现用户登录功能\u0026#34; --allowedTools \u0026#34;Read,Edit,Bash\u0026#34; \u0026amp; claude -w task-2 -p \u0026#34;实现支付接口\u0026#34; --allowedTools \u0026#34;Read,Edit,Bash\u0026#34; \u0026amp; claude -w task-3 -p \u0026#34;编写单元测试\u0026#34; --allowedTools \u0026#34;Read,Edit,Bash\u0026#34; \u0026amp; # 等待所有任务完成 wait echo \u0026#34;All tasks completed\u0026#34; 每个 claude -p 在自己的 Worktree 中独立运行，Shell 的 \u0026amp; 让它们并行执行。这种方式的灵活性最高——你可以完全控制任务分配、错误处理和结果收集。\n典型应用场景 CI/CD 集成：在 GitHub Actions 中自动审查 PR、生成提交信息。\n1 2 3 4 5 6 # GitHub Actions 示例 - name: AI Code Review run: | git diff main | claude -p \\ \u0026#34;You are a code reviewer. Report any bugs or issues.\u0026#34; \\ --output-format json \u0026gt; review.json 仓库级批量操作：对每个子目录执行相同任务。\n1 2 3 4 5 6 7 # 对每个微服务目录并行生成 API 文档 for dir in services/*/; do claude -w \u0026#34;docs-$(basename $dir)\u0026#34; \\ -p \u0026#34;Generate API documentation for this service\u0026#34; \\ --allowedTools \u0026#34;Read\u0026#34; \u0026amp; done wait Bare 模式加速启动：加上 --bare 跳过 hooks、skills、MCP 等自动发现，加快 CI 环境的启动速度：\n1 claude --bare -p \u0026#34;Summarize this file\u0026#34; --allowedTools \u0026#34;Read\u0026#34; Headless 的定位 Headless 是底层能力而非并行协调器。它不管理任务依赖、不协调通信——这些需要你在脚本中自行处理。当你需要比内置并行模式更精细的控制时（如自定义调度逻辑、集成到现有流水线），Headless 就是你的工具。\n第五层：/batch Skill /batch 是一个内置 Skill，专门处理仓库级别的机械重构。它的工作方式是：把一个大型变更拆成 5-30 个 Worktree 隔离的子代理，每个自动开一个 Pull Request。\n使用方式 1 /batch 把所有 JS 文件迁移到 TypeScript 1 /batch 将项目中所有 class 组件重写为函数组件 它做了什么 分析变更范围：Claude 评估任务涉及的文件数量和依赖关系 拆分为子任务：将大任务拆成 5-30 个独立的子任务 并行执行：每个子任务在独立的 Worktree 中由一个子代理执行 自动开 PR：每个子任务完成后自动创建 Pull Request 适用场景 仓库级别的机械重构（批量重命名、统一代码风格） API 迁移（旧接口 → 新接口的批量替换） 依赖升级（批量修改 import 路径、更新配置文件） 代码规范化（统一错误处理模式、统一日志格式） 与其他方式的关系 /batch 本质上是子代理 + Worktree 的打包使用，不是一个独立的协调风格。如果你的任务不是\u0026quot;同一件事重复很多遍\u0026quot;，就不适合用 /batch。\n第六层：Dynamic Workflows 动态工作流 当前面的方式都不够用——任务大到子代理的逐轮调度无法协调，或者需要多轮交叉验证——Dynamic Workflows 登场。\nDynamic Workflows 是 Claude Code 最新的大规模并行方案（Research Preview）。它的核心思想是：让计划住在脚本里，而不是 Claude 的上下文里。\n与其他方式的本质区别 子代理 Agent Teams Dynamic Workflows 计划在哪里 Claude 的上下文窗口 Lead 的上下文窗口 JavaScript 脚本文件 谁决定下一步 Claude，逐轮判断 Lead，逐轮分配 脚本，按代码逻辑执行 中间结果存在哪 Claude 的上下文窗口 共享任务板 脚本变量 可重复性 低（每次重新规划） 中（团队定义可复用） 高（脚本是可审查、可重跑的代码） 规模 几个子代理 3-10 个成员 几十到上百个代理 工作原理 你描述任务：在 prompt 中包含 ultracode 关键词，或直接说\u0026quot;用 workflow 来做\u0026quot; Claude 写脚本：自动生成一个 JavaScript 编排脚本 后台运行：运行时在后台执行脚本，你的会话保持可用 结果汇总：所有代理完成后，只把最终结果返回给你 ultracode: audit every API endpoint under src/routes/ for missing auth checks Claude 会高亮 ultracode 关键词确认，然后编写编排脚本并运行。\n关键能力：对抗性验证（Adversarial Review） Dynamic Workflows 不只是\u0026quot;开更多代理\u0026quot;。它可以编排一种质量模式：让独立的代理互相审查对方的发现，过滤掉不可靠的结论。\n典型流程：\n多个代理并行调研同一问题 代理之间交叉验证（Agent A 审查 Agent B 的结论） 投票表决每个结论的可信度 只输出通过验证的结果 内置的 /deep-research 就是一个 Dynamic Workflow：\n1 /deep-research Claude Code 的 Dynamic Workflows 和 Agent Teams 有什么区别？ 它会：多角度搜索 → 获取源内容 → 交叉验证 → 投票过滤 → 输出带引用的报告。\n管理运行 1 /workflows # 查看运行中和已完成的 workflow 运行中可以暂停（p）、停止单个代理（x）、查看代理详情（Enter）。已完成的 workflow 可以保存为命令（s），以后通过 /\u0026lt;name\u0026gt; 直接调用。\n约束 约束 值 原因 最大并发代理数 16 限制本地资源占用 单次运行最大代理数 1,000 防止无限循环 运行中用户输入 不支持 如需人工审批，分阶段运行 适用场景 500 个文件的仓库级迁移 需要多角度交叉验证的代码审计 从多个独立角度调研后权衡的架构决策 任何\u0026quot;单次子代理调度搞不定\u0026quot;的大规模任务 全景对比总结 方法 是否需要新终端 规模 计划在哪里 适合场景 多终端多实例 ✅ 需要 2-3 你自己 临时应急 Git Worktree ✅ 需要 2-5 你自己 独立大任务并行 子代理 ❌ 不需要 几个 Claude 上下文 辅助探索、计划制定 Agent Teams ❌ 不需要 3-10 Lead 上下文 复杂协作、竞争性调试 Headless + 脚本 ❌ 不需要 自定义 你的脚本 CI/CD、自定义调度 /batch ❌ 不需要 5-30 Claude 自动拆分 仓库级机械重构 Dynamic Workflows ❌ 不需要 几十~上百 JS 脚本 大规模审计、交叉验证 辅助工具（不是独立的并行方式）：\nWorktree：文件隔离，被上述多种方式组合使用 Agent View claude agents：后台会话管理仪表盘，适合\u0026quot;派出去稍后看结果\u0026quot; 小结 Claude Code 的并行能力从基础到进阶，可以归纳为以下层级：\nWorktree——文件级别的隔离，适合你手动管理的并行任务 子代理——任务级别的自动并行，\u0026ldquo;干完活汇报\u0026rdquo; Agent Teams——团队级别的协作并行，成员间可以讨论和协调 Headless + 脚本——完全自定义的批量并行，适合 CI/CD 和高级编排 /batch——一键拆分大型机械重构，自动开 PR Dynamic Workflows——脚本驱动的大规模并行，支持交叉验证 选择建议：\n简单辅助任务 → 子代理 需要深度参与 → Worktree 开独立会话 复杂多模块协作 → Agent Teams CI/CD 自动化 → Headless 模式 仓库级批量重构 → /batch 大规模审计/调研 → Dynamic Workflows 参考资料 并行运行代理 - Claude Code 官方文档 使用 worktrees 运行并行会话 - Claude Code 官方文档 Orchestrate teams of Claude Code sessions - Claude Code 官方文档 Manage multiple agents with agent view - Claude Code 官方文档 Run Claude Code programmatically - Claude Code 官方文档 Orchestrate subagents at scale with dynamic workflows - Claude Code 官方文档 ","permalink":"https://pengkangzhen.github.io/posts/claude-code-parallel/","summary":"Claude Code 提供了多层级的并行能力，从基础的 Git Worktree 隔离到子代理自动调度，再到 Agent Teams 多代理协作。本文系统梳理这些并行模式，帮你告别排队等任务。","title":"Claude Code 并行模式：从 Worktree 到 Agent Teams"},{"content":"核心比喻 你开了一家餐厅，厨师是 LLM，需要厨师按固定格式出菜单。 不同技术 = 不同约束厨师的方式，从\u0026quot;靠自觉\u0026quot;到\u0026quot;逐字监控\u0026quot;。\n第一层：API 层面（厨师用不同的下单工具） 1.1 Prompt 工程（口头吩咐） 你对厨师说：\u0026#34;请用 JSON 格式写下订单，包含菜名和份量\u0026#34; 没有任何工具，全靠厨师自觉 厨师可能写成 {\u0026quot;菜名\u0026quot;: \u0026quot;土豆丝\u0026quot;}（字段名不对） 甚至可能写成 \u0026quot;土豆丝，中份\u0026quot;（根本不是 JSON） 最通用，但最不可靠 1.2 JSON Mode（要求用标准格式纸） 1 2 3 4 5 client.chat.completions.create( model=\u0026#34;gpt-4o-mini\u0026#34;, messages=[...], response_format={\u0026#34;type\u0026#34;: \u0026#34;json_object\u0026#34;}, # 只要求输出合法 JSON ) 给厨师一张标准格式纸，要求\u0026quot;必须写 JSON\u0026quot; 只保证输出是合法 JSON，不保证字段名/结构正确 厨师可能写成 {\u0026quot;dish_name\u0026quot;: \u0026quot;土豆丝\u0026quot;}（你期望的是 dish） 对应 LangChain：llm.with_structured_output(Schema, method=\u0026quot;json_mode\u0026quot;) 1.3 Function Calling（用点菜机） 1 2 3 4 5 6 7 8 9 10 11 12 client.chat.completions.create( model=\u0026#34;gpt-4o-mini\u0026#34;, messages=[...], tools=[{ \u0026#34;type\u0026#34;: \u0026#34;function\u0026#34;, \u0026#34;function\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;place_order\u0026#34;, \u0026#34;parameters\u0026#34;: Order.model_json_schema(), # schema 作为点菜机的选项 } }], tool_choice={\u0026#34;type\u0026#34;: \u0026#34;function\u0026#34;, \u0026#34;function\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;place_order\u0026#34;}}, ) 给厨师一台点菜机，机器里有固定选项 厨师按按钮选择，机器自动输出标准格式 API 层面强制 schema 约束，字段名和类型一定正确 对应 LangChain：llm.with_structured_output(Schema, method=\u0026quot;function_calling\u0026quot;) 1.4 Structured Outputs（智能点菜机 + 实时校验） 1 2 3 4 5 client.beta.chat.completions.parse( model=\u0026#34;gpt-4o-mini\u0026#34;, messages=[...], response_format=Order, # 直接传 Pydantic 模型 ) 最新最强的点菜机，厨师每选一个选项，机器实时校验是否符合规则 底层使用约束解码（Constrained Decoding）——每生成一个 token 都保证合法 最严格：不可能输出不符合 schema 的内容 对应 LangChain：llm.with_structured_output(Schema, method=\u0026quot;json_schema\u0026quot;) 注意：不是所有模型/Provider 都支持全部方式。例如 Qwen 思考模式只支持 Function Calling，不支持 JSON Mode 与思考模式的组合。\n第二层：底层引擎（点菜机是怎么工作的） 约束解码（Constrained Decoding） 核心思想：模型预测下一个 token 时，把不合法的 token 概率直接设为 0。\n模型输出到此时：{\u0026#34;dish\u0026#34;: \u0026#34;酸辣土豆丝\u0026#34;, \u0026#34;size\u0026#34;: \u0026#34; ↓ 模型预测下一个 token 的概率分布： \u0026#34;大份\u0026#34; → 0.3 ✅ 合法 \u0026#34;中份\u0026#34; → 0.5 ✅ 合法 \u0026#34;小份\u0026#34; → 0.2 ✅ 合法 \u0026#34;超级辣\u0026#34; → 0.1 ❌ schema 中 size 是枚举，不包含此值 约束解码：直接把 \u0026#34;超级辣\u0026#34; 概率设为 0 ↓ 最终只会从 [\u0026#34;大份\u0026#34;, \u0026#34;中份\u0026#34;, \u0026#34;小份\u0026#34;] 中选择 这个技术的开源实现：\n工具 约束方式 适用场景 llama.cpp Grammar（BNF 文法） 本地部署开源模型 Outlines JSON Schema / 正则 → 有限状态机 学术研究、灵活定制 vLLM Guided Decoding 生产级推理，支持 JSON/Regex/Grammar LMQL SQL 风格声明式语法 实验性探索 第三层：后处理 / 中间件（厨师写完后的质检） 模型输出后做校验，不通过就打回去重写。\n模型输出 → 解析 → Pydantic 校验 → ├─ 通过 → 返回结构化对象 ✅ └─ 失败 → 把错误信息拼回 prompt → 让模型重试 🔄 主要工具 工具 特点 Instructor Pydantic 校验 + 自动重试，装饰器风格 Guardrails AI 定义 schema，校验不通过自动重生成 Marvin 轻量级，函数装饰器风格 LangChain with_structured_output 统一接口，封装上述所有 method 第四层：Prompt 层面（不依赖任何 API 特性） 最原始但最通用的方法——纯靠提示词描述输出格式：\n请严格按照以下 JSON 格式输出，不要输出其他内容： { \u0026#34;dish\u0026#34;: \u0026#34;菜名（字符串）\u0026#34;, \u0026#34;size\u0026#34;: \u0026#34;大份/中份/小份（三选一）\u0026#34; } 兼容性最好（任何 LLM 都能用），但最不可靠（模型可能不听话）。\n可靠性对比总览 可靠性：弱 ──────────────────────────────────────────────→ 强 Prompt 工程 JSON Mode Function Calling Structured Outputs (口头吩咐) (标准格式纸) (点菜机) (智能点菜机+实时校验) ↑ Constrained Decoding (llama.cpp/Outlines/vLLM) 后处理兜底 (Instructor/Guardrails) 实践建议 优先用约束最强的方法：json_schema \u0026gt; function_calling \u0026gt; json_mode \u0026gt; prompt 工程 不同 Provider 兼容性不同：不是所有模型都支持所有方式，需要逐个测试 后处理作为最后防线：即使用了强约束，也建议加 Pydantic 校验 + 修复 思考模式与结构化输出有冲突风险：思考 token 可能挤占输出预算，且不同 Provider 的兼容性差异大 参考资源 OpenAI Structured Outputs 官方文档 OpenAI Function Calling 官方文档 Qwen Function Calling 文档 Outlines 论文与 GitHub ","permalink":"https://pengkangzhen.github.io/posts/structured-output-techniques/","summary":"结合\u0026rsquo;餐厅点菜\u0026rsquo;比喻，系统梳理 4 层结构化输出技术：从 Prompt 工程到约束解码，从 API 层到后处理中间件。","title":"LLM 结构化输出技术：从原理到实践"},{"content":"背景 手里有一台 MacBook Air 和一台 Windows 台式机，连接着同一个 Wi-Fi。很多时候我想在 Mac 上直接操作 Windows——比如跑个脚本、传个文件——但又不想走到台式机前去操作。SSH 是最轻量的解决方案。\n什么是 SSH SSH（Secure Shell，安全外壳协议）让你安全地远程登录另一台电脑，并在上面执行命令。所有传输的数据都是加密的，不会被窃听或篡改。\n一个类比：SSH 就像一条加密的隧道，你在这头输入命令，另一头的电脑收到并执行，然后把结果安全地传回来。\nSSH vs 远程桌面 对比 SSH 远程桌面 (RDP) 传输内容 命令行（纯文本） 图形界面（屏幕画面） 带宽要求 极低 较高 操作方式 键盘敲命令 鼠标点击图形界面 适用场景 服务器、开发、自动化 日常办公、图形软件 SSH 更轻量高效，适合命令行操作；远程桌面适合需要图形界面的场景。\nWindows 端：启用 OpenSSH 服务器 Windows 10/11 自带 OpenSSH，只需要几步开启。\n1. 安装并启动 SSH 服务 以管理员身份打开 PowerShell，执行：\n1 2 3 4 5 6 7 8 # 安装 OpenSSH 服务器 Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 # 启动 SSH 服务 Start-Service sshd # 设置开机自启 Set-Service -Name sshd -StartupType Automatic 2. 确认防火墙放行端口 22 安装时通常会自动配置防火墙规则，可以验证一下：\n1 2 3 4 5 # 查看已有的 SSH 防火墙规则 Get-NetFirewallRule -Name *ssh* # 如果没有规则，手动添加 New-NetFirewallRule -Name sshd -DisplayName \u0026#39;OpenSSH Server\u0026#39; -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 3. 查看 Windows 的 IP 地址 1 ipconfig 找到无线局域网适配器（或以太网适配器）下的 IPv4 地址，类似 192.168.x.x，后面连接要用。\n4. 确认用户名 1 2 whoami # 例如输出 DESKTOP-ABC\\pkz → 用户名为 pkz Mac 端：连接 打开终端，先测试网络是否通：\n1 nc -zv 192.168.x.x 22 然后连接：\n1 2 ssh 用户名@192.168.x.x # 例如：ssh pkz@192.168.31.143 首次连接会提示确认主机指纹，输入 yes 回车，然后输入 Windows 账户的密码即可。连接成功后，终端就变成了 Windows 的命令行。\n进阶：密钥登录（免密码） 每次连接都要输入密码很麻烦，而且密码可能在输入时被看到。SSH 密钥登录通过一对密钥（公钥 + 私钥）来认证，配置好之后连接不再需要密码。\n原理 简单来说：你在 Mac 上生成一对密钥，把公钥放到 Windows 上，私钥留在 Mac 上。连接时，SSH 用私钥做签名，Windows 用公钥验证——匹配则通过，整个过程不需要传输密码。\n类比：公钥像是一把锁（装在 Windows 上），私钥是唯一的钥匙（留在 Mac 上）。只有拿着钥匙的人才能开锁。\n1. 在 Mac 上生成密钥对 1 ssh-keygen -t ed25519 -C \u0026#34;mac-to-windows\u0026#34; 一路回车即可（使用默认路径 ~/.ssh/id_ed25519，不设密码短语）。\n生成两个文件：\n~/.ssh/id_ed25519 — 私钥（绝不能给别人） ~/.ssh/id_ed25519.pub — 公钥（可以放到任何服务器上） 2. 把公钥复制到 Windows Windows 的 OpenSSH 默认从 C:\\Users\\用户名\\.ssh\\authorized_keys 读取公钥。在 Mac 上执行：\n1 2 3 4 5 # 在 Windows 上创建 .ssh 目录（如果不存在） ssh 用户名@192.168.x.x \u0026#34;mkdir C:\\Users\\用户名\\.ssh 2\u0026gt;nul\u0026#34; # 把公钥追加到 authorized_keys cat ~/.ssh/id_ed25519.pub | ssh 用户名@192.168.x.x \u0026#34;cat \u0026gt;\u0026gt; C:\\Users\\用户名\\.ssh\\authorized_keys\u0026#34; 或者手动方式：把 id_ed25519.pub 的内容复制出来，在 Windows 上用记事本粘贴到 C:\\Users\\用户名\\.ssh\\authorized_keys。\n3. 测试免密登录 1 ssh 用户名@192.168.x.x 不再提示输入密码，直接进入——配置成功。\n如果仍然要求密码，检查 Windows 端：\nauthorized_keys 文件内容是否正确（一整行，不能有换行断裂） 路径中的用户名是否和你登录的用户名一致 进阶：用 SCP 传文件 SSH 连通之后，可以用 scp（Secure Copy）在两台电脑之间安全地传输文件，原理和 SSH 相同——数据全程加密。\n从 Mac 传文件到 Windows 1 2 3 4 scp 本地文件路径 用户名@192.168.x.x:目标路径 # 例：把 Mac 桌面上的报告传到 Windows 桌面 scp ~/Desktop/report.pdf pkz@192.168.31.143:C:/Users/pkz/Desktop/ 从 Windows 传文件到 Mac 1 2 3 4 scp 用户名@192.168.x.x:远程文件路径 本地路径 # 例：把 Windows 上的实验结果拉到 Mac scp pkz@192.168.31.143:C:/Users/pkz/Desktop/results.csv ~/Downloads/ 传整个文件夹 加 -r（recursive）参数即可递归传输整个目录：\n1 2 3 4 5 # 把整个项目文件夹从 Mac 推到 Windows scp -r ~/projects/my_project pkz@192.168.31.143:C:/Users/pkz/Desktop/ # 把 Windows 上的文件夹拉到 Mac scp -r pkz@192.168.31.143:C:/Users/pkz/Desktop/data ~/Downloads/ SCP vs 其他传文件方式 方式 优点 缺点 SCP 加密、命令行一行搞定 传大量小文件较慢 微信/QQ传文件 有图形界面 大小限制、文件名会变、慢 U盘 不依赖网络 要走过去插 共享文件夹（SMB） 可在 Finder 中直接访问 配置稍复杂 日常传几个文件用 SCP 就够了。如果频繁双向同步大量文件，可以考虑配置 SMB 共享文件夹。\n进阶：SSH 配置文件简化连接 每次都要输入 ssh pkz@192.168.31.143 太长了。可以在 ~/.ssh/config 中写好别名：\n配置方法 在 Mac 上创建或编辑 ~/.ssh/config：\n1 nano ~/.ssh/config 添加以下内容：\nHost win HostName 192.168.31.143 User pkz IdentityFile ~/.ssh/id_ed25519 保存后，连接只需：\n1 ssh win SCP 也同样简化：\n1 2 scp report.pdf win:C:/Users/pkz/Desktop/ scp win:C:/Users/pkz/Desktop/results.csv ~/Downloads/ 如果 IP 会变 家庭路由器通常用 DHCP，重启后 IP 可能变化。两个解决办法：\n在路由器管理页面给 Windows 绑定固定 IP（推荐，一劳永逸） 每次连接前先在 Windows 上 ipconfig 确认 IP，然后更新 config 常见问题 连接超时？ 检查两台电脑是否在同一局域网、Windows 防火墙是否放行了 22 端口、sshd 服务是否在运行。\n密码正确但被拒绝？ 确认使用的是 Windows 本地账户的用户名和密码，而不是 Microsoft 账户。如果用的是 Microsoft 账户登录 Windows，SSH 的密码也是那个 Microsoft 账户密码。\n密钥登录不生效，还是要求密码？ 检查 authorized_keys 的内容是否完整（一整行），以及 Windows 端 sshd_config 中 PubkeyAuthentication 是否为 yes。\nWindows 重启后连不上？ 确认 sshd 的启动类型是 Automatic（前面配置过），重启后服务会自动启动。\n","permalink":"https://pengkangzhen.github.io/posts/ssh-mac-to-windows/","summary":"在同一局域网下，通过 OpenSSH 从 Mac 终端远程登录 Windows 台式机，执行命令行操作。","title":"通过 SSH 从 Mac 远程控制 Windows"},{"content":"一、 两阶段随机规划的一般形式 两阶段随机规划通常以最小化成本为目标。其核心思想是：首先做出第一阶段决策 $x$，随后随机事件 $\\xi$ 发生，基于 $x$ 和 $\\xi$，我们再做出第二阶段（补救/追索）决策 $y$。\n（1）数学模型\n$$ \\begin{aligned} \\min_{x} \\quad \u0026 c^T x + \\mathbb{E}_{\\xi}[Q(x, \\xi)] \\\\ \\text{s.t.} \\quad \u0026 Ax = b \\\\ \u0026 x \\ge 0 \\end{aligned} $$其中，$Q(x, \\xi)$ 是第二阶段的最优值函数（Recourse Function），定义为：\n$$ \\begin{aligned} Q(x, \\xi) = \\min_{y} \\quad \u0026 q(\\xi)^T y \\\\ \\text{s.t.} \\quad \u0026 W(\\xi)y = h(\\xi) - T(\\xi)x \\\\ \u0026 y \\ge 0 \\end{aligned} $$（2）符号解释说明\n第一阶段（Here-and-Now）变量与参数：\n$x$：第一阶段决策变量。是在不确定性揭晓之前必须做出的决定（例如：建厂选址、初始库存量）。 $c$：第一阶段决策的成本系数向量。 $A$：第一阶段约束条件的系数矩阵。 $b$：第一阶段约束条件的右端项向量。 $Ax = b, x \\ge 0$：第一阶段的可行域。 随机参数：\n$\\xi$：随机向量（Random Vector），代表不确定性（例如：未来的需求、天气、价格）。它包含了所有随机参数 $(q, h, T, W)$ 的实现值。 $\\mathbb{E}_{\\xi}[\\cdot]$：关于随机变量 $\\xi$ 的数学期望算子。 第二阶段（Wait-and-See）变量与参数：\n$y$：第二阶段决策变量（Recourse Variables）。是在观察到 $\\xi$ 的具体数值后，为了修正或补充第一阶段决策而做出的决定（例如：紧急采购量、外包量）。 $q(\\xi)$：第二阶段的成本系数向量（可能依赖于 $\\xi$）。 $W(\\xi)$：补救矩阵（Recourse Matrix），定义了第二阶段变量之间的约束关系。 $T(\\xi)$：技术矩阵（Technology Matrix），定义了第一阶段决策 $x$ 如何影响第二阶段的约束。 $h(\\xi)$：第二阶段约束的右端项（通常代表随机需求或资源限制）。 二、随机性价值分析（Value of Stochasticity） 2.1 核心概念：RP, EV, EEV 在评估随机规划模型的价值时，我们通常会对比三种不同的解或目标值。假设我们的目标是最小化（Minimization）。\n（1）RP (Recourse Problem / Stochastic Solution)\n含义：这是真正的随机规划问题的最优解。即直接求解上述\u0026quot;一般形式\u0026quot;模型得到的目标函数值。 解释：RP 考虑了所有可能的情景及其概率，寻找一个综合表现最好的 $x^*$。 符号表示：$z_{RP}$ （2）EV (Expected Value Problem / Mean Value Problem)\n含义：这是期望值问题（或均值问题）。它是一种\u0026quot;偷懒\u0026quot;的做法，即把所有的随机变量 $\\xi$ 替换为它们的期望值 $\\bar{\\xi} = \\mathbb{E}[\\xi]$，然后求解一个确定性的优化问题。 解释：这相当于假设未来只会发生一种\u0026quot;平均情况\u0026quot;。 模型： $$ \\min \\{ c^T x + q(\\bar{\\xi})^T y \\mid Ax=b, W(\\bar{\\xi})y = h(\\bar{\\xi}) - T(\\bar{\\xi})x \\} $$ 我们称这个确定性问题的最优解为 $\\bar{x}_{EV}$。 注意：这里的 EV 通常指求解这个简化问题本身，或者该简化问题的最优值 $z_{EV}$。但在后续计算中，更重要的是拿到它的解 $\\bar{x}_{EV}$。 （3）WS (Wait-and-See)\n含义：完全信息解（或\u0026quot;等待-观察\u0026quot;解）。假设我们拥有\u0026quot;上帝视角\u0026quot;，在做决策 $x$ 之前就已经准确预知了 $\\xi$ 会取什么值。 计算方法：对每一种可能的场景 $\\xi$，分别求解最优的 $x(\\xi)$ 和 $y(\\xi)$，然后计算这些最优值的期望。 $$ z_{WS} = \\mathbb{E}_{\\xi} \\left[ \\min_{x,y} \\{ c^T x + q(\\xi)^T y \\mid \\dots \\} \\right] $$ 解释：这是理论上的成本下界，现实中无法达到，因为我们无法预知未来。 （4）EEV (Expected result of using the EV solution)\n含义：期望值解的期望结果。这是衡量\u0026quot;如果我们无视随机性，只按平均情况做决策，最终在现实（随机）环境中的平均表现会怎样\u0026quot;。 计算方法： 先解 EV 问题，得到决策 $\\bar{x}_{EV}$。 将 $\\bar{x}_{EV}$ 固定，代入到原本的随机规划模型中计算真实成本。 $$ z_{EEV} = c^T \\bar{x}_{EV} + \\mathbb{E}_{\\xi}[Q(\\bar{x}_{EV}, \\xi)] $$ 解释：EEV 反映了\u0026quot;盲目相信平均值\u0026quot;所带来的真实后果。通常 $z_{EEV} \\ge z_{RP}$（因为 $z_{RP}$ 是全局最优的，任何其他解都不会比它更好）。 2.2 直观比喻 场景：明天要出门，天气预报说可能下雨。你有一件怕淋湿的贵重衣服，需要决定明天穿不穿。\nEV — \u0026ldquo;赌徒\u0026rdquo; \u0026ldquo;下雨概率 50%，我就当不下雨。\u0026rdquo;\n把随机性拍平成平均值，假装只有一种确定情况。闭着眼做一个决定，然后祈祷。\nEEV — \u0026ldquo;赌徒的代价\u0026rdquo; 赌徒按\u0026quot;不下雨\u0026quot;出了门，结果真下雨了，衣服淋坏了。\nEV 拍脑袋做的决定，放到真实世界里跑一遍的平均代价。也就是\u0026quot;无视随机性，现实会打你多疼\u0026quot;。\nRP — \u0026ldquo;理性人\u0026rdquo; \u0026ldquo;我提前想好了：下雨怎么办，不下雨怎么办，综合权衡后做个最优决定。\u0026rdquo;\n把所有可能情景都考虑进去，找整体期望最优的策略。比如带一把小折叠伞——不下雨时有点累赘，但下了雨也不至于太惨。RP 就是你能做的最好。\nWS — \u0026ldquo;上帝\u0026rdquo; \u0026ldquo;我提前知道明天一定下雨。\u0026rdquo;\n拥有完美预测，针对每个具体情景做出该情景下最完美的决策。现实中做不到，但给出了理论下界——再怎么努力也不可能比上帝做得更好。\n一句话总结：\n概念 一句话 成本 EV 假装不确定性不存在 $z_{EV}$ EEV 用 EV 的决定去面对真实世界 $z_{EEV}$（最贵） RP 考虑所有可能性，理性决策 $z_{RP}$（你能做到的最好） WS 上帝视角，提前知道一切 $z_{WS}$（最便宜） 关系：上帝 ≤ 理性人 ≤ 赌徒的代价，即 $z_{WS} \\le z_{RP} \\le z_{EEV}$。\nVSS = 赌徒的代价 − 理性人 = 你因为\u0026quot;用了随机规划\u0026quot;省了多少钱 EVPI = 理性人 − 上帝 = 你愿意花多少钱买一份\u0026quot;完美天气预报\u0026quot; 三、 经典基准值与评价指标 除了上述三个概念，还有两个非常著名的概念用于衡量随机规划的价值和信息的价值：WS、VSS 和 EVPI。\n2. VSS (Value of the Stochastic Solution) 含义：随机解的价值。它衡量了使用复杂的随机规划模型（RP）比使用简单的均值模型（EV）能节省多少成本。 公式： $$ VSS = z_{EEV} - z_{RP} $$ 解释： 如果 $VSS$ 很大，说明随机性对结果影响巨大，必须使用随机规划。 如果 $VSS \\approx 0$，说明用平均值代替随机变量造成的损失很小，也许没必要用复杂的随机规划。 3. EVPI (Expected Value of Perfect Information) 含义：完全信息的期望价值。它衡量了\u0026quot;如果我们能提前预知未来\u0026quot;，我们愿意为此支付多少钱。 公式： $$ EVPI = z_{RP} - z_{WS} $$ 解释： $z_{RP}$ 是我们在不确定环境下能做到的最好结果。 $z_{WS}$ 是拥有完美情报下的结果。 两者的差值就是不确定性带来的\u0026quot;信息成本\u0026quot;。 总结：不等式关系 对于最小化问题，上述概念之间存在如下经典的链式不等式关系：\n$$ z_{WS} \\le z_{RP} \\le z_{EEV} $$ $z_{WS} \\le z_{RP}$：拥有完美信息（WS）永远比在不确定下做决策（RP）成本更低（或相等）。 $z_{RP} \\le z_{EEV}$：随机规划的最优解（RP）永远比盲目使用均值解（EEV）成本更低（或相等）。 四、示例 我们使用一个经典的**\u0026ldquo;报童问题\u0026rdquo;（Newsvendor Problem）或者简单的工厂生产问题**作为例子。这是一个典型的两阶段随机规划问题。\n1. 场景设定与符号定义 假设你经营一家工厂，生产某种特定零件。\n第一阶段（决策阶段）：\n决策变量 $x$：今天决定生产多少个零件。 参数 $c$：单位生产成本（例如：10元/个）。 随机事件（不确定性）：\n随机变量 $\\xi$：明天的客户需求量（记为 $d$）。 情景（Scenarios）：假设我们预测明天需求有三种可能： 情景 1（低需求）：需求 $d_1 = 100$，发生概率 $p_1 = 0.25$ 情景 2（中需求）：需求 $d_2 = 200$，发生概率 $p_2 = 0.50$ 情景 3（高需求）：需求 $d_3 = 300$，发生概率 $p_3 = 0.25$ 第二阶段（补救/追索阶段）： 明天需求 $d$ 揭晓后，你的生产量 $x$ 可能不匹配，需要补救：\n情况 A（生产过多 $x \u003e d$）：产生库存积压。 变量 $y^+$：积压数量。 参数 $h$：单位库存持有成本（例如：2元/个）。 情况 B（生产过少 $x \u003c d$）：产生缺货，需要紧急外包购买。 变量 $y^-$：缺货数量。 参数 $q$：单位紧急外包成本（通常很高，例如：20元/个）。 2. 求解 EV (Expected Value Problem) 的流程 核心思想：无视需求的波动，假设明天需求一定会是\u0026quot;平均值\u0026quot;。\n计算平均需求： 计算随机变量的期望值 $\\bar{d}$。 $$ \\bar{d} = (100 \\times 0.25) + (200 \\times 0.50) + (300 \\times 0.25) = 200 $$ 建立确定性模型： 假设需求固定为 200，建立优化模型： $$ \\min \\quad 10x + (\\text{基于需求是200的第二阶段成本}) $$ 求解： 在这个确定性模型中，为了不产生库存也不缺货，最优决策显然是生产量等于平均需求。 得到 EV问题的解：$\\bar{x}_{EV} = 200$。 得到 EV 值： 计算该模型下的目标函数值（通常参考意义不大，重点是拿到了 $\\bar{x}_{EV}$ 这个决策）。 3. 求解 EEV (Expected result of using the EV solution) 的流程 核心思想：评估\u0026quot;如果我们按平均值生产（$\\bar{x}_{EV}=200$），在现实世界中会发生什么？\u0026quot;\n固定第一阶段决策： 强制令生产量 $x = 200$（来自 EV 的解）。 代入所有情景计算实际后果： 情景 1（需求 100）： 你生产了 200，需求只有 100。 结果：积压 $y^+ = 100$。 成本 $Cost_1 = (生产成本 \\times 200) + (库存成本 \\times 100)$。 情景 2（需求 200）： 你生产了 200，需求也是 200。 结果：完美匹配。 成本 $Cost_2 = (生产成本 \\times 200) + 0$。 情景 3（需求 300）： 你生产了 200，需求是 300。 结果：缺货 $y^- = 100$。 成本 $Cost_3 = (生产成本 \\times 200) + (外包成本 \\times 100)$。 计算期望总成本： 将上述三个成本按概率加权求和。 $$ z_{EEV} = p_1 \\times Cost_1 + p_2 \\times Cost_2 + p_3 \\times Cost_3 $$ 这个 $z_{EEV}$ 就是如果你盲目相信平均值，最终平均要付出的代价。 4. 求解 RP (Recourse Problem) 的流程 核心思想：在一开始就考虑到所有可能的情景，寻找一个\u0026quot;最稳健\u0026quot;的生产量 $x$。\n建立随机规划模型： 构建一个包含所有情景的大模型。目标是最小化\u0026quot;第一阶段成本 + 第二阶段的期望成本\u0026quot;。 $$ \\min_x \\quad 10x + \\sum_{s=1}^{3} p_s \\times Q(x, d_s) $$ 其中 $Q(x, d_s)$ 是在情景 $s$ 下的最优补救成本。 展开模型（确定性等价形式）： 实际上，求解器会同时解出 $x$ 以及每种情景下的 $y^+_s, y^-_s$。 目标函数变成： $$ \\min \\quad 10x + 0.25(2y^+_1 + 20y^-_1) + 0.50(2y^+_2 + 20y^-_2) + 0.25(2y^+_3 + 20y^-_3) $$ 约束条件包括针对每个情景的供需平衡（例如：$x + y^-_1 - y^+_1 = 100$ 等）。 求解： 使用线性规划求解器求解。 由于外包成本（20元）远高于库存成本（2元），模型通常会倾向于多生产一点，以避免高昂的缺货惩罚。 假设求解得到最优解 $x^* = 240$（这是一个假设值，具体取决于成本参数比例）。 得到 RP 值： 将 $x^* = 240$ 代入目标函数计算出的总成本，即为 $z_{RP}$。 总结对比 指标 含义 决策依据 流程简述 EV 期望值问题 假设需求固定为平均值 (200) 求平均需求 $\\to$ 解确定性模型 $\\to$ 得到决策 $\\bar{x}_{EV}$ EEV 均值解的期望结果 使用 EV 的决策 ($\\bar{x}_{EV}=200$) 固定 $x=200$ $\\to$ 放入所有随机情景 $\\to$ 算加权平均成本 RP 随机规划最优解 考虑所有情景分布 建立包含所有情景的大模型 $\\to$ 直接求出最优 $x^*$ 和期望成本 通常你会发现：$z_{RP} \u003c z_{EEV}$。 这两个值的差值（$VSS = z_{EEV} - z_{RP}$）就是你因为使用了随机规划模型（而不是简单拍脑袋用平均值）而节省下来的钱。\n在同一个工厂生产（报童）的例子中，WS (Wait-and-See，等待-观察解/完全信息解) 代表了一种理想化的\u0026quot;上帝视角\u0026quot;。\n它的核心假设是：在做第一阶段决策 $x$（生产量）之前，你已经拥有了完美的情报，准确预知了明天具体会发生哪种情景。\n以下是求解 WS 的具体流程：\n1. 核心思想 既然我已经预知了明天需求是多少，我就不需要去\u0026quot;猜\u0026quot;或者\u0026quot;权衡\u0026quot;风险了。我会针对每一个具体的情景，分别做出那个情景下最完美的决策。\n2. 求解流程 我们需要针对三种情景，分别求解三个独立的小问题，最后算期望。\n针对情景 1（已知需求 $d_1 = 100$）\n情报：上帝告诉你\u0026quot;明天需求绝对是 100\u0026quot;。 决策：为了成本最低，你显然会生产 $x_1 = 100$。 后果：没有库存积压，也没有缺货。 成本：$Cost_1 = 10 \\times 100 = 1000$ 元。 针对情景 2（已知需求 $d_2 = 200$）\n情报：上帝告诉你\u0026quot;明天需求绝对是 200\u0026quot;。 决策：你会生产 $x_2 = 200$。 后果：完美匹配。 成本：$Cost_2 = 10 \\times 200 = 2000$ 元。 针对情景 3（已知需求 $d_3 = 300$）\n情报：上帝告诉你\u0026quot;明天需求绝对是 300\u0026quot;。 决策：你会生产 $x_3 = 300$。 后果：完美匹配。 成本：$Cost_3 = 10 \\times 300 = 3000$ 元。 3. 计算 WS 值 WS 值就是这些\u0026quot;拥有完美情报下的最优成本\u0026quot;的期望值（加权平均）。\n$$ \\begin{aligned} z_{WS} \u0026= p_1 \\times Cost_1 + p_2 \\times Cost_2 + p_3 \\times Cost_3 \\\\ \u0026= 0.25 \\times 1000 + 0.50 \\times 2000 + 0.25 \\times 3000 \\\\ \u0026= 250 + 1000 + 750 \\\\ \u0026= 2000 \\end{aligned} $$所以，在这个例子中，$z_{WS} = 2000$。\n4. WS 与 RP 的关键区别（直观理解） 在 RP（随机规划）中： 你必须在需求揭晓之前定下一个唯一的 $x$（比如 $x=240$）。这个 $x$ 必须同时应对三种可能，所以它在情景1里会积压，在情景3里会缺货。它是一种妥协的产物。\n在 WS（完全信息）中： 你的 $x$ 是动态的（$x$ 依赖于 $\\xi$）。情景1来了你产100，情景3来了你产300。你永远不会犯错。\n5. 进阶概念：EVPI (完全信息的价值) 通过算出 WS，我们就可以计算 EVPI (Expected Value of Perfect Information)：\n$$ EVPI = z_{RP} - z_{WS} $$ $z_{RP}$：是你作为凡人，在不确定环境中能做到的最好结果（比如假设算出来是 2300 元）。 $z_{WS}$：是上帝的结果（2000 元）。 差值 (300元)：代表了**\u0026ldquo;不确定性\u0026quot;本身给你带来的成本**。 这也意味着：如果你想去买一份\u0026quot;绝对精准的市场预测报告\u0026rdquo;，你最多只愿意支付 300 元。如果报告卖 500 元，你就不如直接用 RP 模型去盲猜合算了。 ","permalink":"https://pengkangzhen.github.io/posts/two-stage-stochastic-programming/","summary":"两阶段随机规划的核心概念梳理：RP、EV、EEV、WS、VSS、EVPI 的含义与直觉，附工厂生产数值示例。","title":"两阶段随机规划：从数学模型到直觉理解"},{"content":"你好，世界 欢迎来到我的博客！这是第一篇文章。\n关于这个博客 这个博客使用 Hugo 搭建，部署在 GitHub Pages 上。\n代码示例 1 2 3 4 5 def hello(): print(\u0026#34;Hello, World!\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: hello() 列表 支持 Markdown 语法 自动部署 完全免费 引用 生活就像骑自行车，要保持平衡，就要不断前进。 — 阿尔伯特·爱因斯坦\n感谢阅读！\n","permalink":"https://pengkangzhen.github.io/posts/hello-world/","summary":"这是我的第一篇博客文章","title":"Hello World"},{"content":"在 Python 项目中，生产依赖和开发依赖往往混杂在一起，导致部署环境臃肿。同时，缺乏类型注解的代码在大型项目中维护成本很高。本文分享如何用 Poetry 分离管理这两类依赖，并用 mypy 建立类型安全机制。\n用 Dependency Groups 分离开发依赖 Poetry 提供了 dependency groups 机制，可以将开发阶段才需要的工具与运行时依赖隔离开：\n1 2 3 4 5 6 7 8 9 10 11 # pyproject.toml [tool.poetry.dependencies] python = \u0026#34;^3.10\u0026#34; # 生产环境依赖... [tool.poetry.group.dev.dependencies] mypy = \u0026#34;^1.10\u0026#34; sphinx = \u0026#34;^7.0\u0026#34; sphinx-rtd-theme = \u0026#34;^2.0\u0026#34; autodocsumm = \u0026#34;^0.5\u0026#34; 这样分类的好处：\npoetry install 只安装生产依赖（默认跳过 dev group） poetry install --with dev 才会安装开发工具 部署时不会带入 mypy、sphinx 等不必要的包 其中这几个开发工具的用途：\n工具 用途 mypy 静态类型检查，在运行前发现类型错误 sphinx 从代码和注释生成项目文档 sphinx-rtd-theme ReadTheDocs 风格的文档主题 autodocsumm 自动生成 API 文档摘要 用 mypy 做静态类型检查 为什么需要类型注解 Python 是动态类型语言，但这不代表类型不重要。在一个多文件协作的项目中，缺少类型信息会导致：\n调用方不知道该传什么参数 重构时无法确认改动的完整影响范围 IDE 无法提供准确的自动补全 加上类型注解 改造前，函数签名没有类型信息：\n1 2 def forward_step(self, request): ... 改造后，参数和返回值都有明确的类型声明：\n1 2 3 4 from typing import Dict, Any def forward_step(self, request: Dict[str, Any]) -\u0026gt; Dict[str, Any]: ... 运行 mypy 验证：\n1 poetry run mypy src/ 如果输出 0 errors，说明所有类型注解与实际使用一致。\n带来的收益 更早发现问题：类型错误在编写阶段就被捕获，而不是运行时崩溃 更好的 IDE 支持：自动补全、跳转定义、重构提示都更准确 代码即文档：函数签名本身就说明了参数和返回值的结构，不需要额外注释 小结 用 Poetry 的 group.dev 分离开发依赖，保持生产环境干净 给核心模块加上类型注解，用 mypy 守住类型安全 这两个实践投入不大，但对项目的长期可维护性有明显提升 ","permalink":"https://pengkangzhen.github.io/posts/new/","summary":"介绍如何用 Poetry 的 dependency groups 管理开发依赖，并通过 mypy 为项目添加静态类型检查。","title":"Poetry 开发依赖管理与 mypy 类型检查实践"},{"content":"写在前面 用 AI 辅助编码时（比如 Claude Code、Cursor），AI 经常会给出各种 Shell 命令：export 设置环境变量、source 激活环境、grep 搜索代码……如果不清楚这些命令的含义，就只能盲目复制粘贴。这篇笔记按功能分类梳理这些常见命令，让与 AI 的协作更高效。\n文件与目录操作 cd — 切换工作目录 1 2 3 4 5 cd ~/projects/vrp_solver # 进入项目目录 cd .. # 返回上一级 cd - # 回到上一次所在的目录（高频操作） cd ~ # 回到主目录 pwd # 显示当前路径 运行脚本时，程序会从当前目录读取数据文件，用 cd 确保你在正确的位置。\ncat — 查看文件内容 快速显示文件的全部内容：\n1 2 3 cat config.yaml # 显示文件内容 cat -n config.yaml # 带行号显示 cat a.txt b.txt \u0026gt; c.txt # 合并多个文件 查看大文件时，cat 会一次性全部输出，建议改用 less（分页浏览）或 head/tail（只看头尾）：\n1 2 3 less large.log # 分页浏览，按 q 退出 head -20 large.log # 只看前 20 行 tail -f app.log # 实时追踪文件末尾（看日志常用） 文本搜索与处理 grep — 文本搜索 grep（global regular expression print）在文件或文本中搜索匹配特定模式的行，相当于命令行里的 Ctrl+F。\n基本语法：\n1 grep \u0026#34;关键词\u0026#34; 文件名 常用选项 选项 作用 示例 -i 忽略大小写 grep -i \u0026quot;error\u0026quot; log.txt -n 显示行号 grep -n \u0026quot;timeout\u0026quot; config.py -r 递归搜索子目录 grep -r \u0026quot;GUROBI\u0026quot; ./projects/ -v 反向匹配（排除） grep -v \u0026quot;#\u0026quot; config.txt -l 只输出文件名 grep -l \u0026quot;main\u0026quot; *.py -c 统计匹配行数 grep -c \u0026quot;Optimal\u0026quot; results.txt -E 支持扩展正则 grep -E \u0026quot;A|B\u0026quot; file 正则表达式 grep 支持正则匹配，加 -E 可以使用扩展正则：\n1 2 3 grep \u0026#34;^NODE\u0026#34; instance.vrp # ^ 匹配行首，找以 NODE 开头的行 grep -E \u0026#34;Optimal|Infeasible\u0026#34; log # | 表示 OR，匹配多个关键词 grep \u0026#34;[0-9]\\{4\\}\u0026#34; data.txt # 匹配四位数字 实际场景 1 2 3 4 5 6 7 8 9 # 检查求解器是否找到最优解 grep \u0026#34;Optimal solution found\u0026#34; gurobi.log # 批量检查实验结果 grep \u0026#34;infeasible\u0026#34; result_*.txt # 配合管道使用 history | grep claude env | grep TASK wc — 统计行数/词数/字节数 1 2 3 wc -l data.csv # 统计行数（常用：快速知道数据集有多少行） wc -w readme.md # 统计词数 wc -l *.py # 统计每个 Python 文件的行数 sort — 排序 1 2 3 4 sort results.txt # 按字母排序 sort -n results.txt # 按数值排序（默认是字典序，数字 9 会排在 10 后面） sort -rn results.txt # 按数值倒序 sort -t, -k2 data.csv # 以逗号分隔，按第 2 列排序 uniq — 去重 通常跟 sort 搭配使用（uniq 只去除相邻的重复行）：\n1 2 3 sort names.txt | uniq # 排序并去重 sort names.txt | uniq -c # 去重并统计每个值出现的次数 sort names.txt | uniq -d # 只显示重复的行 cut — 提取列/字段 1 2 3 cut -d, -f1 data.csv # 以逗号分隔，提取第 1 列 cut -d: -f1,3 /etc/passwd # 以冒号分隔，提取第 1 和 3 列 cut -c1-10 file.txt # 提取每行的前 10 个字符 tr — 字符替换 1 2 3 echo \u0026#34;hello world\u0026#34; | tr \u0026#39;a-z\u0026#39; \u0026#39;A-Z\u0026#39; # 转大写 cat file.txt | tr \u0026#39;\\t\u0026#39; \u0026#39;,\u0026#39; # Tab 替换为逗号 cat file.txt | tr -d \u0026#39;\\r\u0026#39; # 删除 Windows 换行符 (\\r) sed — 流编辑器，批量替换 AI 辅助编码中最常见的用法是批量替换文件中的文本：\n1 2 3 4 5 6 7 8 9 10 11 12 # 替换文件中的文本（不会修改原文件，只输出结果） sed \u0026#39;s/old/new/\u0026#39; file.txt # 直接修改文件（-i） sed -i \u0026#39;\u0026#39; \u0026#39;s/old/new/g\u0026#39; file.txt # macOS 写法 sed -i \u0026#39;s/old/new/g\u0026#39; file.txt # Linux 写法 # 删除空行 sed \u0026#39;/^$/d\u0026#39; file.txt # 删除第 5 行 sed \u0026#39;5d\u0026#39; file.txt awk — 模式扫描与处理 功能强大，这里只列最常用的场景：\n1 2 3 4 5 6 7 8 # 按列提取（以空格/tab 分隔） awk \u0026#39;{print $1, $3}\u0026#39; data.txt # 打印第 1 和第 3 列 # 指定分隔符 awk -F, \u0026#39;{print $2}\u0026#39; data.csv # 以逗号分隔，打印第 2 列 # 带条件的处理 awk \u0026#39;$3 \u0026gt; 100 {print $1}\u0026#39; data.txt # 第 3 列大于 100 时，打印第 1 列 组合使用 这些命令配合管道可以完成复杂的数据处理：\n1 2 3 4 5 # 从日志中提取状态码，排序并统计频次 grep \u0026#34;HTTP\u0026#34; access.log | awk \u0026#39;{print $9}\u0026#39; | sort | uniq -c | sort -rn # 统计 CSV 文件中有多少个不同的城市 cut -d, -f3 data.csv | sort | uniq | wc -l 环境与进程管理 export — 设置环境变量 让后续启动的子进程能读取到该变量：\n1 2 3 4 5 # 设置环境变量 export GRB_LICENSE_FILE=/path/to/gurobi.lic # 查看已设置的变量 env | grep GRB 注意区分：\nVAR=value — 只在当前 shell 有效，子进程看不到 export VAR=value — 子进程也能继承 持久化：export 设置的变量在关闭终端后就消失了。要永久生效，需要写入 ~/.zshrc（zsh）或 ~/.bashrc（bash），然后 source 使其生效。\n很多求解器（Gurobi/CPLEX）都通过环境变量读取许可证或配置。\nsource — 在当前 shell 中执行脚本 脚本中的变量和设置会直接影响当前终端环境：\n1 2 3 4 5 # 重新加载 shell 配置（修改 ~/.zshrc 后执行） source ~/.zshrc # 激活 Python 虚拟环境 source venv/bin/activate source 也可以写成 .，两者等价：\n1 . ~/.zshrc # 等同于 source ~/.zshrc 对比：直接运行 ./script.sh 会在新进程中执行，退出后变量失效；source 则保留在当前 shell 中。\n信息查询 history — 查看历史命令 1 2 history # 显示最近命令 history | grep python # 搜索含 \u0026#34;python\u0026#34; 的历史命令 快捷技巧：\n↑/↓ 方向键浏览历史 !! 重复上一条命令 !n 重新执行第 n 条命令 type — 查看命令类型 1 2 3 type cd # cd is a shell builtin type cat # cat is /bin/cat type ll # ll is an alias for ls -l 这引出了一个冷知识：Shell 命令其实分为内置命令（由 shell 自己实现，如 cd、export、source）和外部命令（磁盘上的可执行文件，如 cat、grep、git）。日常使用中不需要区分，但遇到\u0026quot;为什么 cd 找不到可执行文件\u0026quot;这类问题时，type 能帮你快速定位。\n总结 分类 命令 用途 文件与目录 cd, cat, less, head, tail 切换目录、查看文件内容 文本搜索与处理 grep, wc, sort, uniq, cut, tr, sed, awk 搜索、统计、排序、替换、提取 环境与进程 export, source 设置变量、加载脚本 信息查询 history, type 查看历史、查看命令类型 ","permalink":"https://pengkangzhen.github.io/posts/shell-commands/","summary":"AI 辅助编码时经常遇到各种 Shell 命令，本文按功能分类汇总常用命令的用法。","title":"Shell 常用命令速查"}]