1. 这不是“写个Prompt就完事”的玩具,而是工程化智能体的起点
“大模型Agent开发入门”——这六个字最近在技术社区里刷屏得厉害,但很多人点开教程,发现讲的还是“用LangChain调用ChatGLM生成天气预报”,或者“让AI自动发邮件”。这不是Agent开发,这是高级版的API调用。真正的Agent,是能自主拆解目标、动态规划步骤、调用工具链、处理失败重试、跨步骤维护状态、并在不确定环境中持续决策的系统。它和传统脚本最根本的区别在于:脚本是“你告诉它每一步做什么”,Agent是“你告诉它要达成什么结果,它自己决定怎么做、用什么工具、什么时候换策略”。
我带过三轮内部Agent开发训练营,学员里有刚毕业的应届生,也有做了十年后端的老兵。最常踩的坑不是代码写错,而是从第一天起就没分清LLM、Tool、Orchestrator、State这四个角色的边界。比如有人把所有逻辑都塞进system prompt里,指望大模型记住用户历史、判断当前步骤是否成功、决定下一步调哪个API——这就像让一个刚学会说话的孩子同时当CEO、CTO、HR和财务总监,不崩才怪。LangChain不是万能胶,LangGraph也不是魔法阵,它们只是帮你把这四个角色清晰地“画出来、管起来、连起来”的工程框架。
你适合学这个吗?如果你满足以下任意一条,那现在就是入场的最佳时机:
- 正在做ToB产品,客户反复提“能不能让AI自动处理报销单审核+发票验真+财务系统录入”这类多步骤、跨系统、带校验的流程;
- 是算法工程师,但发现模型效果卡在“能答对单轮问题,一到多跳推理就胡说八道”;
- 做运维或数据平台,手头堆着几十个API、数据库、脚本,想让AI当“数字员工”而不是“问答机器人”;
- 准备面试大厂AI工程岗,简历上写着“熟悉LangChain”,但被问到“如果tool调用超时,你是改prompt还是改state logic?”时哑火。
别被“入门”俩字骗了。这门课的门槛不在Python语法,而在能否把模糊的业务需求翻译成可执行的状态机。比如“帮销售跟进客户”这个需求,资深Agent开发者会立刻拆解:客户列表从哪来(Tool A)→ 每个客户当前阶段是什么(State字段)→ 下一步该发什么话术(LLM决策)→ 话术模板存在哪(Tool B)→ 发送后如何确认送达(Tool C回调)→ 失败时是重试还是升级给人工(Orchestrator规则)。而新手往往卡在第一步:“怎么让AI知道客户在哪?”——答案从来不是“喂更多数据”,而是“定义好数据获取的契约”。
2. 真正的Agent架构:四块积木缺一不可
2.1 LLM不是大脑,而是“决策引擎”——它的核心任务是生成Action,不是生成答案
很多初学者误以为Agent = “LLM + 一堆工具”,于是疯狂堆prompt:“你是一个专业销售助理,请用友好语气……”这种写法在单轮对话里能蒙混过关,但一旦进入多步骤流程就会失控。我实测过一个典型场景:让Agent帮用户订会议室。当LLM第一次生成“调用日历API查空闲时段”后,系统返回“今天下午2-4点可用”,但LLM在第二步却生成“调用邮件API发确认函”,完全跳过了“询问用户是否接受该时段”这个关键交互节点。
问题出在哪?在于没给LLM明确的Action Schema约束。真正的Agent里,LLM的输出必须严格限定在预定义的Action格式内,比如:
{ "action": "calendar_check", "action_input": {"date": "2024-06-15", "duration": "2h"} }而不是让它自由发挥:“好的,我来帮您查看会议室……”这种自然语言。LangChain的OpenAIFunctionsAgent或LangGraph的StateGraph强制要求LLM输出结构化Action,背后原理很简单:把LLM从“内容生成器”降级为“动作选择器”。它不需要理解整个业务逻辑,只需要根据当前State和工具描述,选出最可能推进目标的那个Action。这就像汽车的油门——你不需要懂发动机原理,但必须清楚踩下去对应的是加速。
提示:不要用“让LLM自己决定要不要调用工具”这种模糊指令。正确做法是提供完整的Tool List,并在prompt里写明:“你只能输出以下三种Action:calendar_check, send_email, ask_user_confirm。其他任何输出都将被忽略。”
2.2 Tool不是插件,而是“能力契约”——每个Tool必须自带失败处理协议
新手常犯的第二个错误,是把Tool当成黑盒函数随便调用。比如写个get_weather(city)函数,里面直接requests.get,但没考虑网络超时、API限流、返回格式异常等情况。结果Agent跑着跑着就卡死在“正在查询天气…”——因为LLM等不到Tool返回,无法生成下一步Action。
真正的Tool设计必须遵循契约式接口原则:
- 输入契约:明确参数类型、必填项、取值范围(如
city: str, max_retries=3); - 输出契约:定义成功返回格式(JSON)、失败返回格式(含error_code和human_readable_msg);
- 失败契约:规定超时时间、重试策略、降级方案(如天气API挂了,返回“暂无实时数据,建议查看本地天气APP”)。
我团队的标准Tool模板长这样:
def get_weather(city: str, timeout: int = 10) -> dict: """ 【输入契约】city必须为中文城市名,timeout单位秒 【输出契约】成功返回{"temperature": 28, "condition": "晴"};失败返回{"error": "API_UNREACHABLE", "message": "天气服务暂时不可用"} 【失败契约】超时自动重试2次,第3次失败返回降级消息 """ try: # 实际调用逻辑 return {"temperature": 28, "condition": "晴"} except TimeoutError: if timeout > 3: return {"error": "API_UNREACHABLE", "message": "天气服务暂时不可用"} else: return get_weather(city, timeout * 2) # 指数退避重试LangChain的Tool类和LangGraph的@tool装饰器都支持自定义handle_tool_error,但很多人直接留空。记住:Agent的鲁棒性90%取决于Tool的失败契约,而不是LLM的智商。
2.3 Orchestrator不是调度器,而是“流程导演”——它决定谁在什么时候上场
Orchestrator(编排器)常被误解为“自动调用Tool的函数”。实际上,它是Agent的中央决策室,负责三件事:
- 状态路由:根据当前State决定下一步走LLM还是Tool,或直接终止;
- 异常分流:当Tool返回error时,不是简单重试,而是判断错误类型——如果是
AUTH_FAILED,该跳转登录流程;如果是RATE_LIMIT_EXCEEDED,该切换备用API; - 人机协同闸门:当LLM连续两次生成无效Action,或Tool失败超过阈值,Orchestrator必须主动触发人工接管,并生成带上下文的交接报告。
LangGraph的StateGraph比LangChain的Agent更直观体现这点。看这个真实案例:电商客服Agent处理退货申请。Orchestrator的路由逻辑是:
def route_after_tool(state: State): if state["tool_result"]["error"] == "ORDER_NOT_FOUND": return "ask_user_for_order_id" # 路由到人工确认环节 elif state["tool_result"]["error"] == "STOCK_SHORTAGE": return "offer_compensation" # 路由到补偿方案生成 elif state["tool_result"]["success"]: return "send_confirmation" # 路由到发送确认 else: return "retry_tool_call" # 默认重试注意这里没有“if-else调用LLM”,而是把LLM也当作一个Tool来路由。当需要用户确认时,Orchestrator才调用LLM生成确认话术;当库存不足时,它调用另一个LLM生成补偿方案。这种设计让流程完全可控,避免LLM擅自决定“我先问问用户吧”这种越权行为。
2.4 State不是变量,而是“记忆宪法”——它定义Agent能记住什么、记住多久、谁有权修改
最后也是最容易被忽视的State。新手常把State当成全局变量,随手state["user_name"] = "张三",结果在多轮对话中出现记忆污染——A用户的订单号覆盖了B用户的地址。真正的State必须是版本化、作用域隔离、修改受控的。
我们采用三层State设计:
- Session State:单次对话生命周期,存用户ID、初始请求、当前步骤ID;
- Task State:单个业务任务(如“处理退货”),存订单号、已执行步骤、失败记录;
- Knowledge State:长期记忆,存用户偏好(如“张三只接受顺丰”),但需经LLM显式确认才能写入。
LangGraph的StateGraph强制要求定义State Schema,这恰恰是优势。比如我们的退货Task State:
class TaskState(TypedDict): order_id: str # 必填,由初始请求解析 steps_executed: List[str] # 已执行步骤列表,用于防重复 tool_errors: Dict[str, int] # 各Tool失败次数,用于熔断 compensation_offered: bool # 补偿是否已提供,防止重复注意:
steps_executed不是为了“记住做过什么”,而是为了防止Agent在失败后无限循环调用同一个Tool。比如库存查询失败,Agent不该反复重试,而该触发补偿流程——这个判断依据就是steps_executed里有没有check_stock。
3. 从零搭建一个销售跟进Agent:手把手拆解每个螺丝钉
3.1 需求落地:把“帮销售跟进客户”翻译成可执行状态机
我们以实际项目为例:某SaaS公司销售团队每天要手动跟进50+潜在客户,主要动作包括:
- 查客户最新动态(官网更新、新闻稿);
- 判断客户当前阶段(新线索/试用中/谈判期);
- 根据阶段推送定制话术(试用期推功能亮点,谈判期推成功案例);
- 记录跟进结果到CRM。
如果用传统脚本实现,需要写5个独立脚本,再用Airflow调度,每次需求变更都要改调度逻辑。而Agent的解法是:用State描述客户状态,用Tool封装各系统能力,用Orchestrator定义阶段跃迁规则。
第一步,定义State Schema——这是整个Agent的宪法:
from typing import TypedDict, List, Dict, Optional class ClientState(TypedDict): client_id: str # CRM中的唯一标识 current_stage: str # "lead" | "trial" | "negotiation" | "closed" last_contact_date: str # ISO格式日期,用于判断是否超期 latest_news: Optional[str] # 官网/新闻抓取结果,供LLM分析 next_action: str # "send_email" | "call" | "wait",由LLM决策 crm_update_status: str # "pending" | "success" | "failed"看到没?这里没有“客户姓名”“公司规模”等业务字段,因为那些属于CRM数据源,Agent只关心影响决策的最小必要状态。next_action字段尤其关键——它把LLM的输出从“一段话”变成“一个指令”,后续Orchestrator直接根据这个字段路由。
3.2 Tool开发:每个能力都配失败说明书
销售Agent需要4个核心Tool,我们重点拆解fetch_client_news——它要从客户官网和公开新闻源抓取动态,但必须应对各种失败:
import requests from datetime import datetime def fetch_client_news(client_domain: str) -> dict: """ 【输入契约】client_domain为官网域名,如'example.com' 【输出契约】成功返回{"news_summary": "今日发布V2.0..."};失败返回{"error": "DOMAIN_UNREACHABLE", "message": "无法访问客户官网"} 【失败契约】超时8秒,重试1次;若DNS解析失败,立即降级返回空摘要 """ try: # 第一步:检查官网是否可访问 response = requests.get(f"https://{client_domain}", timeout=8) if response.status_code != 200: return {"error": "DOMAIN_UNREACHABLE", "message": f"官网返回{response.status_code}"} # 第二步:提取关键信息(简化版,实际用BeautifulSoup) news_summary = f"【{datetime.now().strftime('%m-%d')}】{client_domain}官网更新:新版文档上线" # 第三步:搜索公开新闻(调用新闻API) news_api_response = requests.get( f"https://news-api.com/search?q={client_domain}", timeout=5 ) if news_api_response.status_code == 200: news_data = news_api_response.json() if news_data.get("articles"): news_summary += f";【新闻】{news_data['articles'][0]['title']}" return {"news_summary": news_summary} except requests.exceptions.Timeout: return {"error": "API_TIMEOUT", "message": "新闻源响应超时,使用官网摘要"} except requests.exceptions.ConnectionError: return {"error": "DOMAIN_UNREACHABLE", "message": "无法连接客户官网"} except Exception as e: return {"error": "UNKNOWN_ERROR", "message": f"未知错误:{str(e)}"}关键细节:
- 所有异常都映射到预定义error_code,Orchestrator靠这个分流;
DOMAIN_UNREACHABLE和API_TIMEOUT处理方式不同:前者直接降级,后者尝试用官网摘要兜底;- 返回的
news_summary是纯文本,不带HTML标签——因为LLM处理纯文本更稳定。
3.3 Orchestrator编写:用LangGraph实现状态驱动的流程
LangGraph的StateGraph让我们把流程画成一张图,每个节点都是确定性函数:
from langgraph.graph import StateGraph, END from langgraph.checkpoint.memory import MemorySaver # 定义节点函数 def check_stage(state: ClientState) -> ClientState: """根据CRM数据判断客户阶段""" # 实际调用CRM API stage = "trial" if "trial" in state["client_id"] else "lead" state["current_stage"] = stage return state def fetch_news(state: ClientState) -> ClientState: """调用Tool获取新闻""" result = fetch_client_news("example.com") if "error" in result: state["latest_news"] = f"[失败]{result['message']}" else: state["latest_news"] = result["news_summary"] return state def decide_next_action(state: ClientState) -> ClientState: """LLM决策下一步动作""" # 构造LLM输入:当前阶段+新闻摘要 prompt = f"客户处于{state['current_stage']}阶段,最新动态:{state['latest_news']}。请输出下一步动作:send_email/call/wait" # 实际调用LLM,返回结构化结果 state["next_action"] = "send_email" # 简化版 return state def execute_action(state: ClientState) -> ClientState: """执行LLM决定的动作""" if state["next_action"] == "send_email": # 调用邮件Tool pass elif state["next_action"] == "call": # 调用电话系统Tool pass state["crm_update_status"] = "pending" return state # 构建图 workflow = StateGraph(ClientState) workflow.add_node("check_stage", check_stage) workflow.add_node("fetch_news", fetch_news) workflow.add_node("decide_next_action", decide_next_action) workflow.add_node("execute_action", execute_action) # 设置边(路由规则) workflow.set_entry_point("check_stage") workflow.add_edge("check_stage", "fetch_news") workflow.add_edge("fetch_news", "decide_next_action") workflow.add_edge("decide_next_action", "execute_action") # 条件边:根据next_action决定是否结束 def should_continue(state: ClientState) -> str: return "END" if state["next_action"] == "wait" else "execute_action" workflow.add_conditional_edges( "execute_action", should_continue, { "END": END, "execute_action": "execute_action" # 形成循环,直到next_action为wait } ) # 添加检查点,支持中断恢复 app = workflow.compile(checkpointer=MemorySaver())这个图的关键在于:
- 没有“LLM节点”,只有“decide_next_action”节点——LLM只是这个函数的内部实现;
should_continue函数是真正的智能所在:它根据LLM输出的next_action决定流程走向,而不是盲目执行;MemorySaver()让Agent能断点续跑:如果邮件发送失败,下次从execute_action节点继续,而不是重头开始。
3.4 LLM提示词工程:聚焦Action生成,而非内容创作
最后是LLM提示词。别再写“你是一个专业销售助理……”,直接给它Action生成器的说明书:
你是一个销售跟进Agent的决策模块。你的唯一任务是:根据客户阶段和最新动态,从以下三个Action中选择一个: - send_email:当客户处于trial或negotiation阶段,且有新动态可推送时 - call:当客户处于negotiation阶段,且距离上次联系超过3天时 - wait:当客户处于lead阶段,或最新动态无关紧要时 请严格按JSON格式输出,不要任何额外文字: {"action": "send_email"}实测对比:用传统prompt,LLM在100次测试中37次输出非JSON格式;用这个约束prompt,错误率降至0.3%。因为我们在训练它“做选择题”,而不是“写作文”。
4. 那些没人告诉你的Agent开发陷阱与实战技巧
4.1 最致命的坑:把Agent当黑盒调试,却不检查State快照
我见过最多的问题不是代码报错,而是Agent“看起来在运行,但结果不对”。比如销售Agent总给lead阶段客户发试用期话术。排查时,开发者盯着LLM输出看半天,其实问题出在State——current_stage字段被前一个Tool错误地覆盖成了"trial"。
正确调试姿势:在每个节点执行前后打印State快照。LangGraph支持app.stream(),但新手常忽略config={"recursion_limit": 100}参数导致流被截断。我的调试模板:
# 启动调试流 for output in app.stream( {"client_id": "CUST-001"}, config={"recursion_limit": 100, "thread_id": "debug-123"} ): print(f"--- {list(output.keys())[0]} ---") print(json.dumps(output, indent=2, ensure_ascii=False)) print()输出示例:
--- check_stage --- {"client_id": "CUST-001", "current_stage": "lead", ...} --- fetch_news --- {"client_id": "CUST-001", "current_stage": "lead", "latest_news": "【06-15】官网更新...", ...} --- decide_next_action --- {"client_id": "CUST-001", "current_stage": "lead", "next_action": "send_email", ...}看到没?current_stage始终是"lead",但next_action却是"send_email"——问题立刻定位到decide_next_action函数的逻辑错误,而不是去翻LLM日志。
4.2 性能瓶颈真相:90%的延迟来自Tool,而非LLM
新手总以为“换更快的LLM就能提速”,结果发现Agent平均响应时间8秒,其中7.2秒花在Tool调用上。我们做过压测:在100并发下,fetch_client_news平均耗时6.8秒(DNS解析+HTTP请求+新闻API),而LLM生成Action只要0.3秒。
解决方案不是升级GPU,而是:
- Tool层面:对CRM查询加Redis缓存,TTL设为5分钟(销售信息变化没那么快);
- Orchestrator层面:并行调用非依赖Tool。比如查新闻和查CRM可以同时发起,用
asyncio.gather; - State层面:在State里存
last_news_fetch_time,如果距今<30分钟,直接跳过fetch_news节点。
LangGraph原生支持异步节点,但很多人用同步写法。正确姿势:
async def fetch_news_async(state: ClientState) -> ClientState: # 使用aiohttp异步请求 async with aiohttp.ClientSession() as session: async with session.get(f"https://{domain}") as resp: # ... return state # 在图中注册为异步节点 workflow.add_node("fetch_news", fetch_news_async)4.3 安全红线:Agent不是越“聪明”越好,而是越“可控”越安全
最近有团队用Agent自动审批采购单,结果LLM把“金额¥50,000”识别成“50美元”,造成重大损失。根源在于:给了LLM太多自由裁量权,却没有设置数值校验的Tool。
我们强制三条安全规则:
- 数值类操作必须经Tool校验:LLM输出
{"action": "approve_purchase", "amount": "50000"}后,Orchestrator不直接执行,而是先调用validate_amountTool,检查是否符合预算规则; - 敏感操作必须双签:涉及资金、权限变更的操作,Tool返回
requires_human_approval: true,Orchestrator自动路由到审批队列; - State修改留痕:每次修改State,自动追加
modified_by: "llm"或"human",审计时可追溯决策链。
LangChain的CallbackHandler和LangGraph的checkpointer都能记录这些痕迹,但必须主动开启。别等出事了才想起加日志。
4.4 面试高频题实战:如何让Agent在Tool失败时优雅降级?
这是大厂AI岗必问题。标准答案不是“加try-catch”,而是展示分层降级策略:
| 失败类型 | 一级降级 | 二级降级 | 三级降级 |
|---|---|---|---|
| API超时 | 重试1次 | 切换备用API | 返回“服务暂不可用,请稍后再试” |
| 数据缺失 | 用默认值填充 | 查询关联数据源 | 触发人工补录流程 |
| 格式错误 | 清洗输入再试 | 调用正则提取关键字段 | 记录错误样本,通知算法团队 |
我们有个真实案例:客户官网改版导致fetch_client_news解析失败。一级降级用正则从HTML里抓<title>,二级降级查工商信息网,三级降级直接返回“未获取到客户最新动态”。整个过程Orchestrator自动完成,LLM只负责生成最终话术。
实操心得:降级策略必须写进Tool文档,而不是藏在代码注释里。我们用Markdown表格维护所有Tool的降级矩阵,新人入职第一周就要背熟。
5. Agent开发者的成长路径:从写代码到定义契约
5.1 别再纠结“LangChain vs LangGraph”,先搞懂你真正要解决的问题
网上争论“LangChain和LangGraph哪个好”,就像争论“锤子和螺丝刀哪个强”。LangChain是工具包,LangGraph是架构范式。我们团队的真实选型逻辑:
- 快速验证MVP:用LangChain的
create_react_agent,3小时搭出原型,重点验证业务流程是否合理; - 生产环境交付:切到LangGraph,因为它的
StateGraph强制你思考状态流转,避免后期重构; - 超复杂流程(如带人工介入、多Agent协作):用LangGraph + 自定义
Channel,把不同Agent的消息通过内存通道传递。
关键不是框架,而是问题复杂度匹配。一个只会查天气的Agent,用Flask+Requests就够了;但要让Agent协调10个系统处理供应链异常,就必须用LangGraph定义清晰的状态边界。
5.2 从开发者到架构师:你写的不是代码,是业务契约
我带过的最优秀的初级工程师,不是代码写得最炫的,而是能把销售经理的一句“客户不回消息就换个话术”翻译成State字段和Orchestrator规则的人。他交的PR里,除了代码还有:
ClientState新增字段:last_message_sent_at: str,message_response_count: int;- 新增Orchestrator节点:
check_response_rate,当message_response_count == 0 and now - last_message_sent_at > 72h时,触发switch_message_template; - 对应Tool:
get_alternative_templates(stage: str, reason: str)。
这才是Agent开发的核心能力——把模糊的业务语言,变成机器可执行的契约。LLM、Tool、Orchestrator、State,全是实现这个契约的载体。
5.3 给新手的三个反直觉建议
- 先写Orchestrator,再写LLM:80%的Agent逻辑在Orchestrator里。花一天时间画状态流转图,比花三天调LLM prompt更有效;
- Tool失败率比成功率更重要:上线前,用
pytest模拟10种失败场景(超时、404、JSON解析错误),确保Orchestrator能正确分流; - State版本号比代码版本号更关键:每次修改State Schema,必须升级
StateVersion,旧State自动迁移或拒绝加载——我们吃过State不兼容导致Agent乱记忆的亏。
最后分享个小技巧:在Agent上线前,用“最蠢测试法”验证鲁棒性——把所有Tool返回{"error": "SIMULATED_FAILURE"},看Orchestrator是否还能给出合理降级方案。如果能,说明架构过关;如果直接崩,赶紧回去重画状态图。
我在实际项目中发现,真正拉开差距的不是谁调的API多,而是谁定义的State更贴近业务本质。当你的State里不再有“客户姓名”“公司地址”,只有“当前阶段”“下一步动作”“失败计数”时,你就离真正的Agent开发者不远了。