1. 别急着装框架:先搞懂AI Agent到底在解决什么问题
“AI Agent 入门:新手和转行者先学什么,别一上来就啃框架”——这句话我去年在三个不同城市的线下技术分享会上都听到过,每次台下都有至少三分之一的人举手说:“我就是那个一上来就 pip install langchain、clone crewai demo、跑通了 hello world 却完全不知道自己在干嘛的人。”这不是个例,而是当前 AI 学习圈里最典型的“框架幻觉”:以为装上一个 agent 框架,调通几个 API,就能做出能自主思考、能规划、能调用工具的智能体。结果呢?写出来的代码像一串被强行拼接的乐高,缺逻辑、没状态、容错为零,换个输入就崩,部署到真实业务里连个客服自动回复都撑不过两轮对话。
AI Agent 的本质,不是“会调 API 的 Python 脚本”,而是具备目标导向性、状态持续性、工具可调度性与环境可感知性的闭环决策单元。它要能听懂你真正想干什么(比如“帮我订一张下周三从北京飞上海、价格低于800元的机票”),而不是只识别出“订机票”三个字;它要能在查航班、比价格、填信息、确认支付这一连串动作中记住上下文,中间断了能续上,失败了能换方案;它还要知道什么时候该查天气、什么时候该读邮件、什么时候该打开 Excel——这些都不是框架自动给你的能力,而是你必须亲手设计、验证、调试出来的行为逻辑。
所以对新手和转行者来说,真正的入门起点根本不是选 LangChain 还是 LlamaIndex,而是先回答三个问题:第一,我的 Agent 要完成什么具体任务?是自动整理周报、还是帮销售筛选客户线索、还是控制家里的 IoT 设备?任务越具体,边界越清晰,学习路径就越扎实。第二,这个任务里哪些环节必须靠大模型推理?哪些可以硬编码?哪些必须调外部 API?哪些需要本地计算?第三,如果 Agent 在第 3 步卡住了,我怎么知道它卡在哪?是 prompt 写错了?是 tool call 参数传空了?是 API 返回格式变了?还是 memory 里存丢了上一步的订单号?
我带过 27 个零基础转行做 AI 工程师的学员,其中 19 个在前两周就放弃了——不是因为代码难,而是因为他们花三天时间配好了 Dify 的 Docker 环境,却连“为什么这个 Agent 在用户说‘再便宜点’时不会重新搜索低价航班”都说不清楚。后来我们改了路径:第一周不碰任何框架,只用纯 Python + requests + json 写一个“航班查询小助手”,手动拆解 prompt、手动解析返回、手动维护 session_id、手动记录每一步的输入输出。等他们能对着日志文件指出“第 5 次请求时,API 返回了 status=429,但代码没做重试,导致整个流程中断”,才算真正跨过了第一道门槛。这听起来笨,但它是唯一能让你把“Agent 是什么”从概念变成肌肉记忆的方式。
2. 从零搭建一个可调试的 Agent 骨架:比框架更关键的四块基石
很多教程一上来就教你pip install crewai,然后from crewai import Agent,接着贴出 50 行配置代码。这就像教人盖房子,先发一套精装修样板间图纸,却不告诉你承重墙怎么立、地基怎么打、水电管线怎么走。结果就是:你能照着搭出一栋外观漂亮的楼,但只要风大一点、水管爆了、或者想加个阁楼,整栋楼就塌给你看。AI Agent 的底层骨架,其实就四块砖:目标定义层、状态管理层、工具调度层、反馈闭环层。它们不依赖任何框架,用原生 Python 就能实现,而且必须亲手写一遍,才能理解后续所有框架封装的逻辑。
2.1 目标定义层:让 Agent 知道“做到什么才算成功”
这不是写一句“你是一个旅游助手”那么简单。真正的目标定义,要能被程序量化判断。比如“订一张机票”这个目标,必须拆解成可验证的子目标链:
- ✅ 用户明确表达了出发地、目的地、时间、预算(通过 NLU 提取结构化参数)
- ✅ 查询到至少 1 个符合预算的航班(API 返回非空且 price < 800)
- ✅ 用户确认了航班信息(收到“好的,就这个”或类似正向反馈)
- ✅ 支付流程启动并返回订单号(调用支付接口成功)
我在实操中发现,新手最容易忽略的是“用户确认”这一步。他们以为只要 API 返回了航班列表,任务就完成了。但现实是:用户可能说“太贵了,有没有更便宜的?”、“这个时间不行,能改成下午?”——这些不是异常,而是目标未达成的正常信号。所以目标定义层的核心代码,不是 prompt,而是一个is_goal_achieved()函数,它接收当前完整对话历史、已执行动作、最新响应,返回布尔值和失败原因。我通常这样写:
def is_goal_achieved(history: List[Dict], actions: List[Dict], last_response: str) -> Tuple[bool, str]: # 检查是否提取到完整参数 if not all(k in history[-1].get("structured_params", {}) for k in ["origin", "dest", "date", "budget"]): return False, "缺少必要参数:出发地/目的地/日期/预算" # 检查是否已有航班列表且价格合规 flights = [a for a in actions if a["type"] == "search_flights"] if not flights or not any(f["price"] < history[-1]["structured_params"]["budget"] for f in flights[0].get("result", [])): return False, "未找到符合预算的航班" # 检查用户是否确认 user_last_msg = history[-1]["content"] if history else "" if not re.search(r"(好|行|就|确定|ok|yes)", user_last_msg.lower()): return False, "用户尚未确认航班选择" return True, "目标达成"这段代码的价值,不在于它多高级,而在于它强迫你把模糊的“订票成功”翻译成机器可执行的判定逻辑。框架里的goal字段,背后全是这种函数。你不写一遍,永远不知道为什么你的 Agent 在用户说“行”之后还在反复问“您确定吗”。
2.2 状态管理层:让 Agent 记得住“刚才发生了什么”
LangChain 的ConversationBufferMemory或 CrewAI 的Memory看起来很酷,但新手常犯的错误是:把所有历史都塞进去,结果大模型每次都要读 2000 字上下文,token 暴涨,响应变慢,还容易漏关键信息。真正的状态管理,核心是分层+裁剪+标记。
- 分层:把状态分成三类——全局态(用户 ID、偏好设置)、任务态(当前订票流程的 origin/dest/date)、临时态(刚查到的航班列表、待确认的订单号)。它们存储位置、更新频率、序列化方式都不同。
- 裁剪:不是所有对话都要保留。比如用户说“你好”,这种寒暄对订票任务毫无价值,应该被过滤;而“改成下午两点”这种指令,必须精准提取并覆盖原有时间参数。
- 标记:给每条状态打上来源标签(
source: "user_input"/source: "flight_api"/source: "payment_service"),这样当 Agent 出错时,你能一眼看出是用户说错了,还是 API 返回错了,还是 payment 接口挂了。
我推荐新手用一个极简的StateManager类开始:
class StateManager: def __init__(self, user_id: str): self.user_id = user_id self.global_state = {"timezone": "Asia/Shanghai", "language": "zh"} self.task_state = {} # { "booking": { "origin": "PEK", ... } } self.temp_state = {} def update_task_state(self, task_name: str, key: str, value): if task_name not in self.task_state: self.task_state[task_name] = {} self.task_state[task_name][key] = value def get_relevant_history(self, task_name: str, max_tokens: int = 1500) -> str: # 只拼接与当前任务强相关的片段,按时间倒序,优先保留参数变更和 API 结果 history = [] if task_name in self.task_state: for k, v in self.task_state[task_name].items(): if isinstance(v, (str, int, float)): history.append(f"[参数] {k} = {v}") # 添加最近 3 条 API 调用结果摘要 recent_actions = self.get_recent_actions(task_name, limit=3) for act in recent_actions: if act["type"] == "search_flights" and act.get("result"): flight = act["result"][0] history.append(f"[航班] {flight['flight_no']} {flight['dep_time']}->{flight['arr_time']}, ¥{flight['price']}") # 拼接并截断到指定 token 数(粗略估算:1 中文字符 ≈ 2 token) full_text = "\n".join(history) return full_text[:max_tokens//2] # 留出空间给 prompt这个类没有用 Redis、没接向量库,但它让你立刻意识到:状态不是“聊天记录的副本”,而是为当前任务服务的、有结构的、可追溯的数据容器。所有框架的 memory 模块,都是在这个思路上做的增强。你跳过这步直接用现成 memory,就像学开车先背《交通法》却不摸方向盘——理论满分,上路就懵。
2.3 工具调度层:让 Agent 知道“该找谁帮忙”
“Agent 调用工具”听起来很智能,但实际就是一段 if-else + requests 调用。新手常陷入两个误区:一是把所有 API 都包装成 tool,结果 Agent 为了查天气去调用支付接口;二是 tool 的 description 写得像说明书,大模型根本看不懂什么时候该用。
真正的工具调度,关键是意图映射 + 参数校验 + 错误兜底。
意图映射:不是让大模型自己猜“用户说‘看看明天天气’该调哪个 API”,而是你提前定义好 mapping 规则。比如:
TOOL_MAP = { "weather": ["天气", "气温", "下雨", "预报"], "flight_search": ["航班", "机票", "飞", "从...到..."], "payment": ["支付", "付款", "下单", "确认"], }然后写个简单函数,根据用户 query 的关键词匹配到最可能的 tool,再把 query 交给大模型做最终决策。这比纯 LLM 判断稳定十倍。
参数校验:每个 tool 必须有明确的参数 schema。比如
flight_search至少需要origin和dest,缺一不可。我在call_tool函数里强制校验:def call_tool(tool_name: str, params: Dict) -> Dict: required = TOOL_SCHEMA[tool_name]["required"] missing = [r for r in required if r not in params or not params[r]] if missing: return {"error": f"缺少必要参数: {missing}", "retryable": True} # 实际调用...错误兜底:API 失败不能让 Agent 崩溃。要区分:网络超时(可重试)、参数错误(需修正)、服务不可用(降级方案)。我习惯在每个 tool 调用后加一层
handle_tool_error,把原始 error message 转成自然语言提示,比如"天气接口暂时无法访问,请稍后再试",而不是抛出requests.exceptions.Timeout。
这些逻辑,LangChain 的Tool类、CrewAI 的Task都封装了,但封装得越深,你越难 debug。亲手写一遍,你会明白为什么有些 Agent 在 API 返回空数组时就卡死——不是模型问题,是 tool 的 error handling 没写。
2.4 反馈闭环层:让 Agent 学会“哪里没做好”
一个没有反馈机制的 Agent,就像一个闭着眼睛开车的司机。它可能撞上墙,但永远不会知道是方向盘打错了,还是油门踩太猛。反馈闭环,不是简单的“用户说不满意就重来”,而是构建一个可观测、可归因、可迭代的诊断链。
我要求所有新手项目必须包含三个反馈入口:
显式反馈:在每次关键步骤后,主动问用户“这个结果符合您的预期吗?”。不是等用户抱怨,而是把反馈时机前置。代码很简单:
if step == "show_flights": response += "\n\n请问这些航班符合您的要求吗?您可以回复‘是’,或告诉我需要调整的地方(如时间、价格、航空公司)"隐式反馈:分析用户回复的语义倾向。比如用户说“太贵了”,不是负面评价,而是明确的价格敏感信号;说“这个时间不行”,是时间约束未满足。我用一个轻量级规则引擎(不用 BERT,就几条正则)提取这类信号:
feedback_signals = {} if re.search(r"(贵|高|便宜|降价|预算)", user_msg): feedback_signals["price_sensitivity"] = True if re.search(r"(时间|几点|下午|早上|晚)", user_msg): feedback_signals["time_constraint"] = True系统反馈:记录每次 tool 调用的耗时、成功率、返回字段完整性。比如
flight_search平均耗时 1200ms,失败率 8%,其中 60% 是origin参数为空——这说明你的参数提取模块有问题,而不是模型不行。
这三层反馈,共同构成一个FeedbackLoop类。它不生成新 prompt,只生成一份diagnosis_report,告诉开发者:“本次失败主因是参数提取缺失,建议检查 NLU 模块对‘从北京飞上海’的解析逻辑”。这才是真正能驱动迭代的闭环。框架里的callback机制,本质就是这个 report 的自动化版本。你不理解 report 怎么生成,就永远调不好 callback。
3. 实操:用纯 Python 从零实现一个可运行的航班助手 Agent
现在,我们把前面四块基石组装起来,做一个真实的、可运行的、带日志可 debug 的航班助手。全程不依赖任何 Agent 框架,只用标准库 + requests + openai(或任意大模型 SDK)。目标很明确:用户输入“帮我订一张下周三从北京飞上海、价格低于800元的机票”,Agent 能完成查询、展示、确认、支付模拟全流程,并在每一步留下可追溯的日志。
3.1 环境准备与最小依赖
别急着pip install一堆包。这个项目只需要三样东西:
- Python 3.9+
requests(调用 API)openai或dashscopeSDK(调用大模型,这里以 OpenAI 为例,但逻辑完全通用)
安装命令就一行:
pip install requests openai为什么不用 LangChain?因为它会自动帮你做 memory、做 tool parsing、做 output parsing——而我们要亲手控制每一个环节。就像学游泳,先脱掉浮板,才能真正掌握换气节奏。
创建项目结构:
flight_agent/ ├── main.py # 主流程入口 ├── state.py # 状态管理类 ├── tools.py # 工具定义与调用 ├── feedback.py # 反馈分析模块 └── utils.py # 辅助函数(日志、prompt 模板等)提示:所有代码都控制在 200 行以内,重点是逻辑清晰,不是功能炫酷。新手最大的误区是追求“全”,结果哪一块都没吃透。先跑通一个闭环,比堆十个半成品重要十倍。
3.2 核心流程:五步走完一个完整任务
整个 Agent 流程,我把它压缩成五个原子步骤,每个步骤都有明确输入、输出、失败处理:
- NLU 解析:把用户自然语言转成结构化参数
- 目标校验:检查参数是否完整,是否满足业务规则
- 工具调度:调用航班 API,获取结果
- 结果呈现:生成用户友好的展示文本,并附带确认引导
- 反馈处理:解析用户确认/修改指令,决定下一步
这五步不是线性的,而是循环的。比如第 5 步发现用户要改时间,就回到第 1 步重新解析,而不是硬编码“改时间”分支。这种循环结构,才是 Agent 区别于普通脚本的核心。
下面是main.py的主干逻辑(已删减日志和异常处理,保留主干):
def run_agent(user_input: str, user_id: str): # 初始化状态 state = StateManager(user_id) # 第一步:NLU 解析 nlu_result = parse_user_intent(user_input) state.update_task_state("booking", "nlu_result", nlu_result) # 第二步:目标校验 if not validate_booking_params(nlu_result): return "请提供出发地、目的地和日期,例如:'下周三从北京飞上海'" # 第三步:工具调度(查航班) flight_result = call_flight_search( origin=nlu_result["origin"], dest=nlu_result["dest"], date=nlu_result["date"] ) state.update_task_state("booking", "flights", flight_result) # 第四步:结果呈现 response = format_flight_options(flight_result) response += "\n\n请问这些航班符合您的要求吗?您可以回复‘是’,或告诉我需要调整的地方(如时间、价格、航空公司)" # 第五步:等待用户反馈(这里简化为同步等待,实际应异步) user_feedback = input("用户回复: ") # 实际项目中这里是 webhook 或消息队列 # 解析反馈 feedback_analysis = analyze_feedback(user_feedback, flight_result) if feedback_analysis["action"] == "confirm": # 模拟支付 payment_result = simulate_payment(flight_result["selected_flight"]) return f"已为您预订成功!订单号:{payment_result['order_id']},请留意短信通知。" elif feedback_analysis["action"] == "modify": # 更新参数,重新走流程 updated_params = feedback_analysis["updated_params"] state.update_task_state("booking", "origin", updated_params.get("origin", state.task_state["booking"].get("origin"))) # ... 其他参数更新 return "已根据您的要求调整,正在重新查询航班..." else: return "抱歉,我没理解您的意思,请换种方式描述您的需求。" if __name__ == "__main__": print("✈️ 欢迎使用航班助手!请输入您的需求:") while True: user_input = input("> ") if user_input.lower() in ["quit", "exit"]: break result = run_agent(user_input, "demo_user") print(result)这段代码的价值,在于它把“Agent 是什么”具象成了 5 个可打断、可日志、可替换的函数。你可以轻易把call_flight_search换成真实航空 API,把simulate_payment换成真实支付网关,把parse_user_intent换成你训练的 NLU 模型——而整个流程结构不变。这就是骨架的力量。框架的全部价值,就是帮你更快地实现这五个函数,而不是替代它们。
3.3 关键细节:NLU 解析与参数校验的实操技巧
NLU 解析是新手最容易翻车的环节。很多人直接扔给大模型:“请提取出发地、目的地、日期、预算”,结果模型把“下周三”解析成"date": "2024-06-12"(假设今天是 6 月 5 日),但真实业务中,“下周三”必须结合用户时区、节假日、航班排期规则来计算。硬编码规则反而更稳。
我推荐新手用“规则 + 小模型”混合方案:
日期解析:用
dateutil.parser+ 自定义规则。比如:def parse_date(text: str) -> str: # 先匹配常见相对日期 if "今天" in text: return datetime.now().strftime("%Y-%m-%d") if "明天" in text: return (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d") if "下周三" in text: today = datetime.now() # 找到本周三 wed = today + timedelta(days=(2-today.weekday())%7) # 下周三 = 本周三 + 7 天 next_wed = wed + timedelta(days=7) return next_wed.strftime("%Y-%m-%d") # 兜底:交给 dateutil try: return parse(text).strftime("%Y-%m-%d") except: return ""地点解析:维护一个机场三字码映射表(PEK=北京首都,SHA=上海虹桥),比让模型猜准确率高得多。表可以很小,只覆盖高频城市:
AIRPORT_MAP = { "北京": "PEK", "上海": "SHA", "广州": "CAN", "深圳": "SZX", "成都": "CTU", "杭州": "HGH", "西安": "SIA", "重庆": "CKG" }预算解析:用正则提取数字 + 单位,统一转成整数(单位:元):
def parse_budget(text: str) -> int: # 匹配 “800元”、“不超过800”、“低于八百” match = re.search(r"(\d+)[\u4e00-\u9fa5]*[元块]", text) if match: return int(match.group(1)) # 匹配 “八百” 这类中文数字(简单版) cn_num = {"一百":100, "两百":200, "三百":300, "四百":400, "五百":500, "六百":600, "七百":700, "八百":800, "九百":900, "一千":1000} for k, v in cn_num.items(): if k in text: return v return 0
这些看似“土”的方法,在真实业务中比纯 LLM 解析稳定得多。因为它们不依赖模型幻觉,而是基于确定性规则。框架里的LLMChain或Router,底层也是这些规则的封装。你不写一遍,就不知道为什么你的 Agent 在用户说“八百块”时,解析出了 8000。
3.4 可观测性:如何用日志定位 Agent 的每一处卡点
一个无法 debug 的 Agent,就是定时炸弹。我要求所有新手项目必须开启三级日志:
- INFO 级:记录每一步的输入输出,用于复盘流程
- WARNING 级:记录参数缺失、API 超时、模型返回格式异常等可恢复问题
- ERROR 级:记录不可恢复错误,如 API 认证失败、数据库连接中断
日志不是随便 print,而是结构化 JSON,方便后续用 ELK 或 Grafana 分析。utils.py里的日志函数长这样:
import json import logging from datetime import datetime logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('agent.log', encoding='utf-8'), logging.StreamHandler() ] ) def log_step(step_name: str, user_id: str, input_data: dict, output_data: dict, status: str = "success"): log_entry = { "timestamp": datetime.now().isoformat(), "step": step_name, "user_id": user_id, "input": input_data, "output": output_data, "status": status, "duration_ms": 0 # 实际使用时用 time.time() 计算 } logging.info(json.dumps(log_entry, ensure_ascii=False)) # 使用示例 log_step("nlu_parse", "demo_user", {"raw_input": "下周三从北京飞上海"}, {"origin": "PEK", "dest": "SHA", "date": "2024-06-12"})有了这个日志,当用户反馈“Agent 总是查不到航班”时,你不用猜,直接 grep:
grep '"step":"flight_search"' agent.log | grep '"status":"error"'就能看到所有失败的航班查询,进而发现是origin字段经常为空——根源在 NLU 解析没处理好“北京首都机场”这种长名称。这种定位速度,是任何框架文档都给不了的。
注意:日志里绝不记录用户手机号、身份证号等敏感信息。所有 PII(Personally Identifiable Information)字段必须脱敏,比如
"phone": "138****1234"。这是工程基本素养,不是可选项。
4. 框架选型实战指南:什么时候该用,什么时候该绕开
现在你已经亲手搭出了一个可运行、可 debug、可扩展的 Agent 骨架,接下来才是框架登场的时刻。框架不是起点,而是加速器。它的价值,是在你理解了底层逻辑之后,帮你省掉重复造轮子的时间。但选错框架,或者过早引入,反而会掩盖问题,拖慢学习进度。
4.1 框架的本质:封装了什么,又隐藏了什么?
所有主流 Agent 框架,本质上都在封装前面讲的四块基石,但封装深度和侧重点不同:
| 框架 | 封装重点 | 隐藏风险 | 适合场景 |
|---|---|---|---|
| LangChain | Tool 调用、Memory 管理、Prompt 模板 | 把复杂逻辑藏在Chain和AgentExecutor里,debug 时要扒源码 | 快速 PoC、教学演示、中小规模项目 |
| LlamaIndex | 数据索引、RAG 流程、文档加载 | 强依赖向量库,对非文档类任务(如 API 调用)支持弱 | 知识库问答、文档智能助理 |
| CrewAI | 多 Agent 协作、角色分工、任务编排 | 抽象层级过高,单个 Agent 的内部状态难以干预 | 需要多个专业 Agent 协同的复杂流程(如市场分析+竞品报告+PPT生成) |
| Dify | 可视化编排、Web UI、API 发布 | 黑盒程度最高,自定义逻辑必须写插件,调试成本陡增 | 业务人员快速上线、无需写代码的场景 |
我做过一个测试:用同一套业务逻辑(航班助手),分别用纯 Python、LangChain、CrewAI 实现。结果发现:
- 开发时间:纯 Python 3 天,LangChain 1.5 天,CrewAI 1 天,Dify 0.5 天
- 首次上线 bug 数:纯 Python 0(因为每一步都自己写),LangChain 7 个(mostly memory 和 tool parsing 的坑),CrewAI 12 个(角色间状态同步问题),Dify 3 个(但全是插件编写问题)
- 后续迭代速度:纯 Python 最快(改一行代码立刻生效),LangChain 次之,CrewAI 因为抽象层多,改一个参数要查三份文档,Dify 最慢(每次改都要重新发布)
这个数据说明:框架节省的是前期开发时间,但代价是后期 debug 和迭代成本。对新手而言,前期省下的时间,往往在后期十倍奉还。
4.2 新手框架上手路线图:从“看得见”到“管得住”
如果你决定引入框架,我强烈建议按这个渐进路线走,而不是一上来就pip install crewai:
阶段一:LangChain —— 理解“封装”的边界(1~2 周)
目标:用 LangChain 重写你之前写的纯 Python 航班助手,但只用它最基础的组件:Tool、LLM、Memory,禁用所有 Chain 和 AgentExecutor。
为什么?因为AgentExecutor是 LangChain 最“黑盒”的部分,它自动帮你做 tool selection、parsing、loop control。新手用它,就像坐自动驾驶汽车,连刹车在哪都不知道。而Tool和Memory是透明的,你清楚看到自己定义的 tool 是怎么被调用的,memory 是怎么被更新的。
实操任务:
- 把你写的
call_flight_search封装成 LangChainTool - 把
StateManager替换成ConversationBufferMemory,对比两者日志差异 - 手动写一个
agent_loop函数,调用llm.invoke()+tool.run(),而不是AgentExecutor.invoke()
你会发现:LangChain 的Tool其实就是你写的call_tool函数的包装;Memory就是你state.task_state的字符串化版本。一旦看穿这层,框架就不再神秘。
阶段二:CrewAI —— 理解“协作”的代价(2~3 周)
目标:把单 Agent 航班助手,拆成三个 Agent 协作:TravelPlanner(负责目标拆解)、FlightSearcher(负责 API 调用)、PaymentHandler(负责支付模拟)。
为什么?因为 CrewAI 的核心价值在 multi-agent,而不是单 agent。新手常误以为 CrewAI 是“更好用的 LangChain”,其实它是另一个维度的抽象。当你亲手写三个 Agent 的execute_task方法,并处理它们之间的task_output传递时,你会深刻理解:协作不是免费的,它带来状态同步、错误传播、超时控制等一系列新问题。
实操陷阱:
- 不要用
Crew自动调度,先手动agent1.kickoff()→agent2.kickoff(input=agent1.output)→agent3.kickoff(input=agent2.output) - 给每个 Agent 设置独立的
Memory,观察它们如何“忘记”其他 Agent 的中间结果 - 故意让
FlightSearcher返回空结果,看PaymentHandler如何优雅降级,而不是直接崩溃
这一步的目的,不是做出多牛的系统,而是让你建立对“协作复杂度”的敬畏。很多团队用 CrewAI 后性能暴跌,就是因为没走过这一步,盲目相信“自动编排”。
阶段三:Dify —— 理解“低代码”的权衡(1 周)
目标:把你用 CrewAI 写好的三 Agent 流程,在 Dify Web UI 里用可视化节点重新搭建,并发布成 API。
为什么?因为 Dify 代表了另一条技术路径:不写代码,用配置驱动。它的价值在于交付速度,代价是可控性。当你在 Dify 里拖拽“LLM 节点”、“HTTP 请求节点”、“条件分支节点”时,你会直观感受到:每个节点背后,都是你之前亲手写的那些函数。而 Dify 的“插件”机制,其实就是让你把call_flight_search这样的函数,包装成标准 HTTP 接口供平台调用。
实操重点:
- 对比 Dify 生成的 API 请求日志,和你纯 Python 版本的日志,看数据流转是否一致
- 尝试在 Dify 里修改一个 prompt,观察它如何影响最终输出,理解 prompt engineering 在低代码环境中的位置
- 把 Dify 的 workflow 导出为 JSON,读懂它的 DSL(Domain Specific Language),你会发现它和你写的
agent_loop函数逻辑完全对应
这一步完成后,你就拥有了全栈视角:从手写代码,到框架封装,再到低代码编排。这时再选框架,就不是“哪个火用哪个”,而是“哪个最适合当前阶段的目标”。
4.3 避坑清单:新手最常踩的 7 个框架陷阱
基于我带过的 27 个学员的真实踩坑记录,整理出这份血泪清单。每一条,都对应一个具体的 debug 场景:
LangChain 的
AgentExecutor默认开启max_iterations=15,但你的业务流程只需 3 步。结果 Agent 在第 4 步无意义地循环,耗尽 token,返回乱码。
✅ 解决:初始化时显式设置max_iterations=3,并监听AgentFinish事件终止循环。CrewAI 的
Memory默认是ConversationBufferMemory,但它把所有 Agent 的对话混在一起。TravelPlanner的中间思考,被PaymentHandler当成用户指令执行。
✅ 解决:为每个 Agent 创建独立的Memory实例,并在Task中指定context参数,只传递必要信息。Dify 的“知识库”默认用
text-embedding-ada-002,但你的中文文档召回率只有 40%。
✅ 解决:在 Dify 设置里切换 embedding model 为bge-m3或multilingual-e5-large,并重新切分 chunk(中文 chunk_size 设为 128,不是英文的 512)。所有框架的
Tooldescription 都写成“调用航班查询 API”,但大模型根本不懂什么叫“航班查询”。
✅ 解决:description 必须包含输入参数示例和成功返回格式,比如:“输入:{'origin': 'PEK', 'dest': 'SHA', 'date': '2024-06-12'};输出:[{'flight_no': 'CA123', 'price': 780, 'dep_time': '08:00'}]”。用
openai.ChatCompletion时,response.choices[0].message.content可能为空,但框架默认认为一定有内容,导致后续解析 crash。
✅ 解决:在框架外加一层safe_get_content()函数,检查finish_reason是否为stop,否则返回 fallback 文本。**CrewAI 的
Task设置async_execution=True,但