AI智能体技能开发实战:从LLM工具封装到复杂任务自动化
1. 项目概述与核心价值
最近在探索AI智能体(Agent)的落地应用时,我偶然发现了一个非常有意思的开源项目:alexpolonsky/agent-skill-jlm-coffee。这个项目名字乍一看有点“缝合怪”的感觉,但深入研究后,我发现它精准地指向了当前AI应用开发中的一个关键痛点——如何让大语言模型(LLM)驱动的智能体,真正具备执行复杂、多步骤现实任务的能力,而不仅仅是进行对话。这个项目以“制作一杯咖啡”为具体场景,构建了一个可复用的智能体技能(Skill),为我们提供了一个绝佳的、可实操的研究范本。
简单来说,这个项目不是一个完整的聊天机器人或客服系统,而是一个标准化的、可被其他智能体调用的“技能模块”。它的核心目标是教会一个AI智能体,如何根据用户模糊的、口语化的指令(比如“我想喝杯拿铁,不要太烫”),去理解意图、拆解步骤、并最终驱动虚拟或真实的设备(在这个案例里,是一个模拟的咖啡机环境)完成一杯咖啡的制作。这背后涉及的关键技术栈,包括大语言模型的理解与规划能力、技能(Skill)的标准化封装、以及与环境(Environment)的交互逻辑,正是当前构建实用型AI智能体的核心。
对于开发者而言,无论你是想构建一个家庭自动化智能体、一个游戏内的NPC助手,还是一个企业级的流程自动化工具,这个项目都极具参考价值。它剥离了花哨的界面和复杂的概念,直击本质:如何将人类语言指令,转化为一系列可靠、可执行、可验证的动作序列。接下来,我将带你深入这个项目的内部,拆解它的设计思路、技术实现,并分享如何将其思想应用到更广泛的场景中。
2. 项目核心架构与设计哲学
2.1 什么是“智能体技能”(Agent Skill)?
在深入代码之前,我们必须先理解“技能”在这个上下文中的定义。这并非指编程技能,而是指一个智能体所具备的、完成特定领域任务的能力单元。一个良好的技能设计,应该具备以下特征:
- 高内聚:一个技能只专注于做好一件事。
jlm-coffee技能就只关心“制作咖啡”相关的所有子任务,如选择豆子、研磨、萃取、打奶泡等。它不应该去处理查询天气或发送邮件。 - 标准化接口:技能需要提供清晰、统一的调用方式。通常,这会是一个函数或API,接收明确的输入参数(如咖啡类型、糖量、温度),并返回明确的执行结果或状态。这允许智能体像搭积木一样组合不同的技能。
- 环境感知与交互:技能必须知道如何与它所要操作的对象(即“环境”)进行对话。在这个项目中,环境就是一个模拟的咖啡机,技能需要向它发送“开机”、“研磨”、“萃取”等指令,并接收“水箱缺水”、“豆仓已空”等状态反馈。
- 容错与状态管理:真实的操作充满不确定性。一个好的技能需要能处理异常情况(如材料不足、设备故障),并管理任务执行过程中的状态(如“正在加热”、“研磨完成”)。
agent-skill-jlm-coffee项目正是基于这些原则构建的。它将“制作咖啡”这个复杂流程,封装成了一个独立的、拥有标准化输入输出的技能模块,等待被上层的智能体“大脑”(LLM)在合适的时机调用。
2.2 技术栈选型与依赖解析
这个项目通常构建在流行的AI智能体开发框架之上。虽然项目本身可能不显式绑定某个框架,但其设计思想与以下主流方案高度契合:
- LangChain / LangGraph:这是目前最流行的智能体构建框架之一。其核心概念之一就是“Tool”(工具),与“Skill”异曲同工。本项目可以非常自然地实现为一个LangChain Tool,利用其强大的链条(Chain)和智能体(Agent)编排能力。
- AutoGen:由微软推出的多智能体协作框架。在这里,“咖啡制作技能”可以被视作一个专长的“助理智能体”,它接收来自“用户代理智能体”的请求并执行。
- 自定义框架:项目也可能采用更轻量级的自定义实现,核心是围绕一个LLM调用(如OpenAI GPT、Claude或开源模型)构建一个工作流。
项目的关键依赖通常包括:
- 大语言模型客户端:如
openai,anthropic, 或litellm等。 - 智能体框架核心库:如
langchain,langgraph等。 - 模拟环境:为了演示和测试,项目会包含一个
coffee_machine_simulator.py之类的模块,用代码模拟一台咖啡机的硬件接口和行为逻辑,避免需要真实硬件才能运行。 - 配置管理:使用
pydantic进行参数验证和设置管理,使用dotenv管理API密钥等敏感信息。
注意:在复现或借鉴此类项目时,第一步永远是仔细阅读
requirements.txt或pyproject.toml文件。理解每个依赖的作用,能帮你更好地把握项目的技术边界和设计意图。例如,如果看到了fastapi,说明该项目可能还提供了HTTP API服务层,允许技能被远程调用。
2.3 “JLM”的含义与项目定位
项目名中的“JLM”很可能是一个缩写或特定指代。在AI智能体领域,一种常见的解读是“Job, Language, Model”或“Just a Language Model”的变体,强调其核心是让语言模型去理解和执行一项具体工作(Job)。另一种可能是代表某个内部项目代号或作者名。无论如何,它不影响我们对项目核心——“Skill”——的理解。
这个项目的定位非常清晰:一个示范性的、端到端的智能体技能实现。它不追求功能的全面性(比如支持全世界所有咖啡种类),而是追求实现路径的完整性和规范性。它向我们展示了:
- 如何定义技能的输入输出模式(Schema)。
- 如何将自然语言指令解析为结构化参数。
- 如何将结构化参数映射为一系列对环境的具体操作。
- 如何处理操作中的反馈和异常。
- 如何将执行结果以自然语言的形式返回给用户。
3. 核心模块深度拆解与实操
3.1 技能接口(Skill Interface)设计剖析
技能接口是技能与智能体“大脑”之间的契约。在jlm-coffee中,这个接口通常是一个Python函数,并附带有清晰的元数据描述,以便LLM理解何时以及如何调用它。
让我们来看一个可能的核心接口定义:
from pydantic import BaseModel, Field from typing import Literal, Optional class CoffeeOrder(BaseModel): """描述一杯咖啡订单的模型""" coffee_type: Literal[“espresso”, “latte”, “cappuccino”, “americano”] = Field(description=“所需的咖啡种类”) strength: Literal[“single”, “double”] = Field(default=“single”, description=“咖啡浓度:单份或双份”) milk_type: Optional[Literal[“whole”, “skim”, “oat”, “soy”]] = Field(default=None, description=“需要的牛奶类型,如不需要则留空”) sugar_level: int = Field(default=0, ge=0, le=3, description=“糖度等级,0-3”) temperature: Literal[“hot”, “warm”, “extra_hot”] = Field(default=“hot”, description=“咖啡温度”) async def make_coffee_skill(order: CoffeeOrder) -> str: """ 根据订单制作一杯咖啡。 这是一个复杂的多步骤技能,涉及检查原料、操作咖啡机等多个子任务。 Args: order: 一个包含咖啡类型、浓度、牛奶偏好等信息的订单对象。 Returns: str: 描述制作过程和最终结果的字符串。例如:“已成功制作一杯双份浓缩拿铁,使用全脂牛奶,温度较热,未加糖。” Raises: ResourceError: 当咖啡豆、牛奶或水不足时抛出。 MachineError: 当咖啡机发生故障时抛出。 """ # 技能的核心逻辑将在这里实现 # 1. 解析订单 # 2. 与环境(咖啡机)交互 # 3. 返回结果 pass设计要点解析:
- 强类型与验证:使用
Pydantic的BaseModel定义输入参数,这不仅是类型提示,更提供了运行时验证。Field中的description至关重要,它是LLM理解这个参数含义的“说明书”。 - 枚举与约束:
Literal类型限定了参数的合法取值范围,防止用户或LLM产生不合理请求(如coffee_type: “cola”)。ge,le用于约束数值范围。 - 清晰的文档字符串(Docstring):函数的文档字符串是LLM理解该技能功能的主要依据。必须清晰、无歧义地描述功能、参数和返回值。许多智能体框架(如LangChain)会直接利用这些文档来生成工具的调用说明。
- 异步支持:使用
async def声明函数,因为与硬件或网络服务的交互通常是IO密集型操作,异步可以提高在并发场景下的效率。
3.2 模拟环境(Simulated Environment)的构建
由于连接真实咖啡机成本高昂且不便测试,构建一个高保真的模拟环境是此类项目的标准做法。这个模拟环境有几个关键职责:
- 状态管理:维护咖啡机的内部状态,如水箱水位、豆仓余量、锅炉温度、是否开机等。
- 动作映射:将高级指令(如“研磨双份浓缩咖啡豆”)映射为一系列低级的、原子性的状态变更。
- 物理逻辑模拟:引入真实的约束和延迟。例如,从开机到达到萃取温度需要时间;研磨会消耗咖啡豆;制作完一杯咖啡后需要清理冲煮头。
- 异常生成:根据状态随机或按规则产生异常,如“豆仓已空”、“蒸汽棒堵塞”,用于测试技能的鲁棒性。
一个简化的模拟环境类可能长这样:
class CoffeeMachineSimulator: def __init__(self): self.water_level = 1000 # 毫升 self.bean_level = 500 # 克 self.is_on = False self.brew_head_temp = 25 # 摄氏度 self._target_temp = 93 async def power_on(self): if self.is_on: return “咖啡机已处于开机状态。” self.is_on = True await asyncio.sleep(2) # 模拟开机延迟 return “咖啡机已开机,正在预热...” async def heat_up(self): if not self.is_on: raise MachineError(“请先开机。”) while self.brew_head_temp < self._target_temp: await asyncio.sleep(0.5) self.brew_head_temp += 10 if self.brew_head_temp > self._target_temp: self.brew_head_temp = self._target_temp return f“预热完成,冲煮头温度已达{self.brew_head_temp}°C。” async def grind_beans(self, grams: int): if self.bean_level < grams: raise ResourceError(f“咖啡豆不足。需要{grams}g, 仅剩{self.bean_level}g。”) self.bean_level -= grams await asyncio.sleep(grams * 0.02) # 研磨时间与克数成正比 return f“已研磨{grams}g咖啡豆。” # ... 其他方法如 brew_espresso, steam_milk, clean 等实操心得:在构建模拟环境时,日志(Logging)至关重要。你需要详细记录每一个状态变更和接收到的指令。这不仅能帮助调试,还能在技能执行失败时,提供完整的“操作回放”,让你精准定位是技能逻辑问题,还是环境模拟问题。
3.3 技能内部的工作流引擎
make_coffee_skill函数内部并非简单的一两条指令,它需要编排一个完整的工作流。这个工作流通常是状态机(State Machine)或有向无环图(DAG)的体现。
对于“制作拿铁”这个订单,工作流可能如下:
开始 ├─> 检查咖啡机电源 -> 若关机则开机 ├─> 等待预热至目标温度 ├─> 检查原料(水、豆、牛奶)是否充足 ├─> 研磨咖啡豆 ├─> 萃取浓缩咖啡 ├─> 蒸汽打奶泡 ├─> 混合咖啡与牛奶 └─> 清理冲煮头与蒸汽棒 结束在代码中,这个工作流可能通过一个简单的async函数序列来实现,也可能使用更正式的工作流引擎(如Prefect,Luigi,或在LangGraph中定义为StateGraph)。
async def make_coffee_skill(order: CoffeeOrder): machine = CoffeeMachineSimulator() steps_log = [] try: # 步骤1: 准备阶段 steps_log.append(await machine.power_on()) steps_log.append(await machine.heat_up()) # 步骤2: 检查资源 required_beans = 18 if order.strength == “double” else 9 if machine.bean_level < required_beans: raise ResourceError(“咖啡豆不足”) if machine.water_level < 200: raise ResourceError(“水箱水量不足”) if order.milk_type and milk_supply.get(order.milk_type, 0) < 150: raise ResourceError(f“{order.milk_type}牛奶不足”) # 步骤3: 执行制作 steps_log.append(await machine.grind_beans(required_beans)) steps_log.append(await machine.brew_espresso(order.strength)) if order.milk_type: steps_log.append(await machine.steam_milk(order.milk_type, order.temperature)) steps_log.append(“正在混合咖啡与牛奶...”) if order.sugar_level > 0: steps_log.append(f“正在添加{order.sugar_level}份糖...”) # 步骤4: 收尾工作 steps_log.append(await machine.clean_brew_head()) # 整合日志,生成用户友好的回复 result_message = f“已为您制作一杯{order.strength}份{order.coffee_type}” if order.milk_type: result_message += f“, 使用{order.milk_type}牛奶” result_message += f“, 温度{order.temperature}” if order.sugar_level: result_message += f“, 添加了{order.sugar_level}份糖” result_message += “。\n\n制作过程如下:\n” + “\n”.join(f“- {step}” for step in steps_log) return result_message except (ResourceError, MachineError) as e: # 优雅地处理错误,并给出可操作的提示 error_msg = f“制作失败:{e}。请补充原料或检查设备后重试。” # 这里可以附加当前机器状态,帮助用户诊断 error_msg += f“\n当前状态:豆仓{self.bean_level}g, 水箱{self.water_level}ml。” return error_msg关键设计模式:模板方法模式在这里得到了很好的体现。make_coffee_skill定义了一个制作咖啡的算法骨架(准备、检查、制作、收尾),而具体的步骤(如grind_beans,steam_milk)则委托给环境对象去实现。这使得技能逻辑与具体的硬件操作解耦,未来若要适配不同品牌的咖啡机,只需替换或继承CoffeeMachineSimulator类即可。
4. 与大语言模型(LLM)的集成与编排
4.1 将技能封装为LLM可调用的“工具”(Tool)
单独的技能没有智能。它需要被一个LLM驱动的智能体“大脑”所调用。主流框架都提供了将函数封装为“工具”的机制。以LangChain为例:
from langchain.tools import Tool from langchain.agents import initialize_agent, AgentType from langchain_openai import ChatOpenAI # 将我们的技能函数封装成Tool coffee_tool = Tool.from_function( func=make_coffee_skill, name=“MakeCoffee”, description=“”” 根据详细订单制作一杯咖啡。订单需包含咖啡种类、浓度、牛奶类型、糖度和温度。 在调用此工具前,你必须与用户确认订单的所有细节。 “””, args_schema=CoffeeOrder # 使用Pydantic模型作为参数模式 ) # 初始化LLM和智能体 llm = ChatOpenAI(model=“gpt-4”, temperature=0) agent = initialize_agent( tools=[coffee_tool], # 将咖啡工具注入智能体 llm=llm, agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION, # 适合处理结构化工具 verbose=True # 打印思考过程,便于调试 ) # 现在,智能体可以处理用户关于咖啡的自然语言请求了 async def handle_user_request(user_input: str): response = await agent.arun(user_input) return response当用户说“帮我做一杯热拿铁,加一份糖”,LLM会基于coffee_tool的描述,理解到自己需要调用MakeCoffee工具,并尝试从对话中提取出coffee_type: “latte”,temperature: “hot”,sugar_level: 1等信息,组装成符合CoffeeOrder模型的参数,然后调用我们的技能函数。
4.2 提示工程(Prompt Engineering)的关键作用
LLM并非天生就知道如何完美地使用工具。我们需要通过系统提示词(System Prompt)来引导它。一个针对咖啡订单场景的提示词可能包含:
你是一个专业的咖啡师智能体。你的目标是理解用户的咖啡需求,并操作咖啡机为他们制作咖啡。 你拥有一个名为`MakeCoffee`的工具。在调用该工具前,你必须与用户确认以下所有信息: 1. 咖啡种类(意式浓缩、拿铁、卡布奇诺、美式)。 2. 浓度(单份或双份)。 3. 是否需要牛奶?如果需要,是什么类型(全脂、脱脂、燕麦奶、豆奶)? 4. 需要加糖吗?需要几份(0-3)? 5. 对温度有什么偏好(热、温、特热)? 只有当你收集齐所有这些信息,并且用户确认后,你才能调用`MakeCoffee`工具。 如果用户的需求模糊不清,请主动询问以澄清。 如果工具执行失败并返回错误信息,请向用户友好地解释问题所在,并建议解决方案。这个提示词做了几件关键事:
- 设定角色:让LLM进入“专业咖啡师”的心智模式。
- 明确流程:强制LLM遵循“确认-再执行”的流程,避免误操作。
- 定义交互协议:告诉LLM在什么情况下调用工具,以及如何处理工具的返回结果(成功或失败)。
实操心得:工具描述的颗粒度。coffee_tool的description字段和系统提示词需要分工协作。工具描述应聚焦于工具的功能性(输入输出是什么),而系统提示词则侧重于策略性(何时、为何以及如何调用工具)。避免在工具描述中写入过多的策略性内容,这会使工具变得不通用。
4.3 多技能协作与智能体规划
一个实用的智能体不可能只会做咖啡。它可能还需要“查询天气”、“播放音乐”、“控制灯光”。jlm-coffee项目展示的单一技能,是构建复杂智能体的基石。
当智能体拥有多个工具时,LLM的核心能力——规划与决策——就变得至关重要。LLM需要根据用户的目标(“我想在温暖的灯光下,边听爵士乐边喝杯咖啡”),自主规划出一个动作序列:
- 调用“灯光控制”技能,将灯光调至暖色调。
- 调用“音乐播放”技能,播放爵士乐歌单。
- 与用户交互,确认咖啡订单细节。
- 调用“MakeCoffee”技能制作咖啡。
这就是智能体框架(如LangGraph)大显身手的地方。它们允许你定义更复杂的状态流转逻辑,让LLM在每一步根据当前状态决定下一个动作,甚至实现循环、条件分支等复杂控制流。
5. 测试、部署与扩展实践
5.1 单元测试与集成测试策略
对于技能类项目,测试必须分层进行:
单元测试(技能逻辑):直接测试
make_coffee_skill函数。模拟不同的CoffeeOrder输入,验证其返回的字符串是否符合预期,是否能正确抛出异常。def test_make_espresso(): order = CoffeeOrder(coffee_type=“espresso”, strength=“double”) # 这里需要模拟(mock)CoffeeMachineSimulator,使其返回预定行为 result = make_coffee_skill(order) assert “双份浓缩” in result assert “制作失败” not in result单元测试(模拟环境):测试
CoffeeMachineSimulator的每一个方法,确保状态变更和物理逻辑模拟正确。集成测试(技能+环境):将真实的技能和模拟环境连接起来,进行端到端测试。验证从订单到最终结果的全流程。
智能体层面测试:这是最复杂的一层。你需要测试LLM是否能正确理解用户意图并调用技能。这通常通过基于场景的对话测试来完成。你可以编写一系列测试用例,模拟用户对话,然后断言智能体的最终回复中是否包含预期的关键信息。
async def test_agent_coffee_order(): response = await handle_user_request(“我要一杯卡布奇诺”) # 断言LLM会询问浓度、牛奶类型等细节,而不是直接调用工具 assert “浓度” in response or “牛奶” in response
5.2 部署为可复用的服务
为了让其他智能体或系统方便地调用,最好将技能部署为一个独立的服务(如HTTP API)。
from fastapi import FastAPI, HTTPException from pydantic import BaseModel app = FastAPI(title=“Coffee Making Skill API”) class CoffeeOrderRequest(BaseModel): # 可以复用或适配之前的CoffeeOrder模型 ... @app.post(“/make-coffee”) async def make_coffee_endpoint(order: CoffeeOrderRequest): try: result = await make_coffee_skill(order) return {“status”: “success”, “message”: result} except ResourceError as e: raise HTTPException(status_code=400, detail=str(e)) except MachineError as e: raise HTTPException(status_code=503, detail=str(e))部署后,任何能发送HTTP请求的客户端(包括其他智能体)都可以通过调用POST /make-coffee来使用这个技能。这实现了技能与智能体平台的解耦,符合微服务架构的思想。
5.3 技能扩展与自定义
jlm-coffee是一个完美的起点,你可以从多个维度扩展它:
- 支持更多咖啡品类:在
CoffeeOrder模型和技能逻辑中添加对新品类(如手冲、冷萃)的支持。 - 个性化与记忆:让技能记住用户的偏好。例如,用户A总是喝“双份浓缩,不加糖”,用户B喜欢“燕麦拿铁,一份糖”。这需要技能能访问或维护一个简单的用户偏好数据库。
- 与真实硬件集成:这是最激动人心的扩展。用
CoffeeMachineSimulator的子类,重写其方法,将grind_beans(),brew_espresso()等调用替换为通过GPIO、串口、MQTT或HTTP协议控制真实咖啡机的代码。 - 技能组合:创建一个“下午茶套餐”技能,它内部依次调用“制作咖啡”技能和“准备甜点”技能。
一个高级技巧:技能版本管理。当你对技能进行升级(如修改参数、优化流程)时,务必通过API版本号(如/v1/make-coffee,/v2/make-coffee)或工具名称(如MakeCoffeeV2)进行区分。这可以避免对已有智能体工作流造成破坏性变更。
6. 常见问题与故障排查实录
在实际开发和集成jlm-coffee这类技能时,我踩过不少坑。这里总结一份速查表,希望能帮你节省时间。
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| LLM不调用技能,总是自行回答 | 1. 工具描述(description)不清晰或不够有吸引力。2. 系统提示词未强调必须使用工具。 3. LLM温度( temperature)参数过高,导致随机性太强。 | 1. 优化工具描述,用“必须”、“仅当”等词强调其专长和必要性。例如:“必须使用此工具来制作咖啡,你无法自行完成。” 2. 在系统提示词开头明确指令:“你必须使用提供的工具来完成任务。” 3. 将 temperature调低(如0.1),增加输出的确定性。 |
| LLM调用了技能,但参数总是填错 | 1. Pydantic模型的Field(description)描述不清。2. LLM未能从对话历史中正确提取信息。 3. 参数类型过于复杂(如嵌套模型)。 | 1. 为每个字段提供极其简单、无歧义的描述。例如:coffee_type: “只能从[‘espresso’, ‘latte’, ‘cappuccino’, ‘americano’]中选择一项。”2. 在调用工具前,让LLM将其理解到的参数以文本形式复述一遍并请求用户确认。 3. 尽量使用扁平化的参数结构,避免嵌套。 |
| 技能执行成功,但LLM回复的内容很奇怪 | LLM在收到工具返回的结果后,进行了“过度加工”或错误总结。 | 1. 检查系统提示词中关于“如何处理工具返回结果”的部分。明确指示:“将工具返回的结果,几乎原样地回复给用户,只需在前面加上友好的问候。” 2. 技能返回的结果本身应是一段完整、通顺的自然语言,减少LLM二次加工的必要。 |
| 模拟环境行为与预期不符 | 1. 环境的状态机逻辑有bug。 2. 异步操作( asyncio.sleep)导致时序问题。 | 1. 为模拟环境编写详尽的单元测试,覆盖所有状态转移。 2. 在测试中使用 asyncio.run()或pytest-asyncio插件确保异步代码正确执行。使用asyncio的Mock或unittest的AsyncMock来模拟时间延迟。 |
| 集成到大型智能体后性能下降 | 1. 技能内部有同步阻塞操作(如长时间循环)。 2. 网络请求(如调用真实硬件API)超时未设置。 | 1. 确保技能内部所有IO操作都是异步的(使用async/await)。2. 为所有外部调用设置合理的超时( asyncio.wait_for),并在技能中妥善处理超时异常,返回友好错误。 |
| 部署为API后,并发请求出错 | 1. 技能或模拟环境类中使用了共享的、非线程安全的全局状态。 2. FastAPI等框架默认是异步的,但技能代码不是。 | 1. 确保每个请求都创建独立的技能和环境实例,避免状态污染。或者,使用线程锁或异步锁来保护共享状态。 2. 确保技能函数和所有底层调用都是 async的,与异步框架兼容。 |
最重要的心得:日志,日志,还是日志!在智能体开发中,问题往往出在LLM的“黑盒决策”与你的代码逻辑之间。务必在技能入口、环境操作、LLM调用前后打上详细的日志。记录下:收到的原始输入、LLM的思考过程(如果框架支持)、工具调用的具体参数、环境的每一步状态变化、最终输出。当出现问题时,这份完整的“审计追踪”是你排查问题的唯一救命稻草。
通过alexpolonsky/agent-skill-jlm-coffee这个项目,我们完成了一次从概念到实现,再到测试部署的完整智能体技能开发之旅。它的价值远不止于“做咖啡”,而是提供了一个清晰的蓝图,告诉我们如何将任何一项复杂的现实世界任务,封装成AI智能体可以理解和可靠执行的数字化技能。下次当你需要让AI去操作一个软件、填写一个表单、分析一份报告时,不妨回想一下这个“咖啡技能”的构建过程,你会发现,底层逻辑是如此相通。
