☰
AI Agent入门:从零构建可运行的最小认知闭环
2026/10/22 5:52:13 网站建设 项目流程

1. 别被“Agent”三个字吓住:这其实是个“会拆任务的聪明助手”,不是玄学

“AI Agent 入门:新手和转行者先学什么,别一上来就啃框架”——这个标题我看到的第一反应是:太对了。去年带过几个从行政、设计、教培行业转过来的学员,清一色上来就问“LangChain和LlamaIndex哪个好用?”“AutoGen怎么配多智能体?”结果代码还没跑通,人已经在报错日志里迷失方向。他们不是不努力,是用力点错了地方。就像想学开车,先去研究发动机曲轴间隙和ECU刷写协议,而不是先摸方向盘、踩离合、挂挡。AI Agent的本质,是让大模型具备目标导向的自主行动能力,它要能理解你的真实意图、把大目标拆成小步骤、调用工具查资料、修正错误、最终交出结果。这个过程里,大模型是“大脑”,但光有大脑不会走路——它得有“眼睛”(输入解析)、“手”(工具调用)、“记事本”(记忆管理)、“复盘本”(反思机制)。而市面上90%的入门教程,一上来就把“手”和“记事本”的图纸塞给你,却没告诉你怎么先让大脑看懂一句话。所以这篇不是讲LangChain源码,也不是教你怎么部署Ollama,而是还原一个真实的学习路径:从你第一次在终端敲下pip install之前,该建立什么认知框架;从你写出第一行能真正完成任务的代码时,背后到底发生了什么;以及为什么跳过某些环节,后面三个月都在填坑。

核心关键词“AI Agent”“新手”“转行者”“框架”已经划出了边界:我们不谈学术论文里的Agent架构分类,不对比ReAct和Plan-and-Execute的token效率差异,只聚焦于“一个零基础的人,如何在30天内,独立写出一个能自动查天气、总结新闻、生成周报的可运行Agent”。这个目标听起来不小,但拆解下来,它只需要三块砖:你能准确描述问题(Prompt工程)+ 你能让它调用一次外部API(工具集成)+ 你能让它记住上一步干了啥(状态管理)。其余全是这三块砖的组合与加固。很多人卡在第一步,以为Prompt就是“请帮我写个周报”,结果模型回你一篇散文;也有人卡在第二步,调用API时连HTTP状态码401和403都分不清,更别说处理rate limit;还有人卡在第三步,让Agent连续问三个问题后,它突然忘了第一个问题的背景。这些问题,跟框架选型关系极小,跟底层原理关系也不大,全是你动手前该扫清的认知盲区。所以这篇的起点,不是代码,而是你打开编辑器前,脑子里该有的那张地图。

2. 内容整体设计与思路拆解:为什么必须绕开框架,先建“最小可行认知闭环”

2.1 框架不是起点,而是封装后的终点

我见过太多人花两周时间配置LangChain的ChatMessageHistory,结果连requests.get()返回的JSON结构都看不懂。LangChain这类框架的价值,在于它把“调用工具→解析响应→喂给大模型→提取下一步指令”这一整条链路,用类和方法包装成了.invoke()和.agent_executor.run()。这极大提升了开发效率,但代价是:你失去了对每个环节的控制感和调试能力。就像汽车的ABS系统,它能在刹车时自动点刹防抱死,但如果你连“刹车踏板踩下去车轮为什么会转”都不清楚,一旦ABS故障报警灯亮起,你只能干瞪眼。AI Agent的学习路径,必须反着来:先手动实现一遍ABS的机械原理(手动调用API、手动拼接prompt、手动保存上下文),再去看电子控制单元(框架)是怎么把它自动化、标准化的。这样当你用框架时,遇到ToolException: Failed to parse tool response这种报错,你立刻能定位到是工具返回的JSON字段名和prompt里要求的不一致,而不是在框架源码里逐行翻找。

这个“最小可行认知闭环”的设计逻辑非常朴素:任何Agent的核心循环,无非是“思考→行动→观察→反思”四步。我们不追求一步到位,而是用最原始的方式,把这四步走通一次。比如做一个“查今日北京天气并推荐穿衣”的Agent,它的闭环是:

  1. 思考:用户说“今天穿什么”,我需要知道北京当前温度、湿度、风速;
  2. 行动:调用天气API(如OpenWeatherMap)获取数据;
  3. 观察:拿到JSON响应,提取main.temp、weather[0].description等字段;
  4. 反思:把温度数字和“多云”描述,翻译成“建议穿长袖衬衫+薄外套”。

这四步里,第1步靠Prompt写清楚,第2步靠requests库,第3步靠Python字典操作,第4步靠大模型生成。没有框架,没有异步,没有向量数据库,甚至不需要本地部署大模型——用免费的OpenAI API或国内某平台的API就能跑通。这个闭环跑通后,你再去看LangChain的Tool类,就会发现它不过是把第2、3步封装成一个对象;再去看AgentExecutor,就会明白它只是把第1、4步的prompt模板化了。这种“先造轮子,再用轮胎”的路径,看似慢,实则快。因为后续所有框架的学习,你都在做“映射”工作:这个参数对应我手动写的哪一行?这个异常堆栈,对应我当时手写时哪个环节漏了try-except?这种映射能力,才是转行者最稀缺的底层能力。

2.2 新手最大的陷阱:混淆“能运行”和“能理解”

转行者常犯的一个致命错误,是把“代码跑通了”等同于“我学会了”。我辅导过一位从前做UI设计的学员,她用LangChain官方示例代码,五分钟就搭出了一个能查股票的Agent。她很兴奋,觉得“Agent不过如此”。但当我问:“如果API返回的数据格式变了,比如price字段从数字变成了字符串,你的Agent会报错吗?错在哪一行?怎么改?”她愣住了。后来我们一行行debug,发现她根本没看过JsonOutputParser的源码,也不知道tool的args_schema是怎么校验输入的。她的“能运行”,是复制粘贴的结果,不是理解驱动的结果。这种状态下的学习,就像用胶带把两块木板绑在一起——看着连上了,一受力就散。

所以我们的整体设计,强制要求每一个环节都“可打断、可观察、可修改”。比如在“行动”环节,我们不用框架的tool.run(),而是手动写:

import requests def get_weather(city: str) -> dict: url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid=YOUR_KEY" response = requests.get(url) if response.status_code == 200: data = response.json() return { "temp": round(data["main"]["temp"] - 273.15, 1), # 转摄氏度 "condition": data["weather"][0]["description"] } else: raise Exception(f"Weather API error: {response.status_code}")

这段代码里,requests.get()的返回值、response.json()的结构、data["main"]["temp"]的取值路径,全部暴露在你眼皮底下。你改一个字段名,马上能看到KeyError;你少写一个括号,IDE立刻标红。这种“所见即所得”的反馈,是建立直觉的唯一途径。而框架的黑盒性,恰恰切断了这种反馈。因此,整个学习路径的设计哲学就是:用最笨的办法,获得最扎实的肌肉记忆。当你的手指已经条件反射地敲出response.json().get("data", {}).get("items", [])时,你再去学框架的JsonOutputParser,就不是在背API,而是在优化自己的肌肉记忆。

2.3 转行者的独特优势:别把“非技术背景”当短板,它是天然的Prompt工程师

很多转行者焦虑自己没写过代码,但恰恰忽略了自己最大的隐藏资产:对真实业务场景的深刻理解。一个做过三年HR的人,比纯程序员更清楚“员工离职率分析报告”里该包含哪些维度、数据口径怎么统一、管理层最关注哪三个指标。一个教了十年数学的老师,比算法工程师更懂“如何用生活化例子解释梯度下降”。这些不是技术,却是构建高质量Agent的黄金燃料。因为Agent的终极价值,从来不是“它能调用多少个API”,而是“它能解决多复杂的现实问题”。

所以我们的内容设计,刻意强化这种优势的转化。比如在Prompt工程环节,我们不教“用<|endoftext|>分隔指令”,而是带你看一份真实的销售日报邮件,然后一起拆解:“老板说‘看看上月转化率’,他真正想要的是什么?是总转化率?各渠道转化率?还是新老客户转化率对比?邮件里提到的‘活动A效果不佳’,这个‘不佳’怎么量化?是低于8%?还是环比降了15%?”这种拆解,本质上就是把业务语言翻译成机器可执行的指令。而这项能力,资深从业者闭着眼都能做,但刚毕业的CS学生反而要专门训练。因此,整个路径的第二阶段,核心任务是:把你过去十年积累的行业经验,变成Agent的“领域知识库”。我们会教你如何把一份PDF版的《客户服务SOP》提炼成10条结构化规则,再写成Prompt里的system message;如何把Excel里的历史投诉分类表,转换成few-shot examples。这不是编程,这是你最擅长的“理解需求、定义问题、组织信息”的能力迁移。框架学得再熟,没有这个迁移能力,你的Agent永远停留在“玩具”级别。

3. 核心细节解析与实操要点:从Prompt到工具调用,每一步都藏着关键细节

3.1 Prompt工程:不是写作文,是写“可执行的接口说明书”

新手对Prompt最大的误解,是把它当成“对AI说话”。但实际工作中,Prompt是Agent与大模型之间的契约接口。它必须像API文档一样精确:输入格式、输出格式、边界条件、错误处理方式,全部明确定义。我曾帮某电商公司优化客服Agent的Prompt,原版是:“请根据用户问题,友好地回答。”结果模型把“退货地址错了”回复成“亲,您的心情我们完全理解呢~”,完全没解决问题。改成契约式Prompt后:

【角色】你是一名严格遵守《电商客服SOP V3.2》的售后专员。 【输入】用户消息(含订单号、问题类型、关键事实) 【输出格式】JSON,必须包含: - "action": "resolve"|"escalate"|"collect_info" - "reason": 简短说明(不超过20字) - "next_step": 具体操作(如"请提供订单截图"、"已为您生成退货单号:RT2024XXXX") 【约束】 - 若用户未提供订单号且问题需查单,action必须为"collect_info" - 若问题涉及物流超时且订单创建超72小时,action必须为"escalate"

上线后首次响应准确率从42%升至89%。这个案例说明:好的Prompt,核心不在文采,而在结构化约束。

实操中,必须掌握三个关键细节: 第一,system message不是摆设,是Agent的“操作系统内核”。它决定模型的行为基线。很多新手把所有规则塞进user message,导致每次调用都要重复传输,既费token又易出错。正确做法是:system message定义长期角色、规则、格式;user message只传本次动态数据。比如天气Agent的system message应固定为:

你是一个专业天气顾问,只回答与天气相关的问题。所有输出必须是JSON格式,包含"temp_c"、"condition"、"recommendation"三个字段。不回答天气以外的任何问题。

第二,few-shot examples不是教学,是“格式锚点”。新手常堆砌大量例子,以为越多越好。实则2-3个高质量例子,胜过20个模糊例子。关键是例子必须覆盖典型case和边界case。比如:

User: 北京现在几度? Assistant: {"temp_c": 22.5, "condition": "晴", "recommendation": "适合穿短袖"} User: 上海下雨吗? Assistant: {"temp_c": 18.2, "condition": "小雨", "recommendation": "建议带伞"}

这两个例子,直接教会模型:输入是城市+天气属性,输出是三个固定字段的JSON。第三,必须加入“失败兜底”机制。模型不是100%可靠,Prompt里要预设它答错时的退路。比如加一句:“若无法从输入中提取城市名,输出{'error': '请提供具体城市名称'}”。这比事后用try-except捕获JSONDecodeError,更能保障用户体验。

提示:别迷信“高级Prompt技巧”,先确保基础契约成立。我测试过,一个结构清晰、字段明确的Prompt,即使用免费版Qwen,也能稳定输出合规JSON;而一个文采斐然但无格式约束的Prompt,用GPT-4也会随机输出散文。

3.2 工具调用:不是“调API”,是“教Agent理解世界”

工具调用(Tool Calling)常被简化为“写个函数,注册到Agent”。但真实难点在于:如何让Agent准确理解“什么时候该调用,调用时传什么参数”。这需要两层对齐:语义对齐(Agent理解用户意图)和结构对齐(Agent理解API参数规范)。

先看语义对齐。用户说“帮我订明天下午三点的会议室”,Agent要能识别出:这是“预订”动作,对象是“会议室”,时间是“明天15:00”。这依赖于Prompt里对工具功能的精准描述。错误示范:“这是一个预订会议室的工具”。正确写法:

tool_name: book_meeting_room tool_description: 预订指定时间的会议室。仅当用户明确要求预订(如'订'、'预约'、'安排')且提供了时间(如'明天'、'下周三'、'15:00')时才调用。 parameters: - room_id: 字符串,会议室ID(如'A101'、'B205'),若用户未指定则留空 - start_time: ISO8601格式时间字符串(如'2024-05-20T15:00:00'),必须推算出绝对时间

注意这里强调了触发条件(“仅当...才调用”)和参数约束(“必须推算出绝对时间”)。这相当于给Agent装了一个“决策开关”,避免它把“会议室在哪”也当成预订请求。

再看结构对齐。API的参数名、类型、必填项,必须和Agent的认知严格一致。常见坑是:API文档写"start_time": "string",但实际要求ISO8601格式;而Agent生成的参数可能是"start_time": "明天下午3点"。解决方案是:在工具函数内部做强校验和转换,而不是指望Agent一次写对。比如:

def book_meeting_room(room_id: str = "", start_time: str = ""): # 强制转换时间 try: dt = parse_datetime(start_time) # 自定义parse_datetime函数,支持"明天15:00"等自然语言 iso_time = dt.isoformat() except: raise ValueError("start_time格式错误,请使用'YYYY-MM-DD HH:MM'或'明天/后天 HH:MM'") # 调用真实API payload = {"room_id": room_id, "start_time": iso_time} response = requests.post(API_URL, json=payload) return response.json()

这个设计的关键在于:把自然语言理解的复杂性,收口在工具函数内部。Agent只需负责“识别意图+传递原始字符串”,转换工作由工具完成。这样既降低了Agent的负担,又提高了鲁棒性。我实测过,这种设计下,Agent对时间表达的容错率提升60%以上——它能把“下周一上午”、“3天后早上”、“本周五10点”全部交给parse_datetime处理,自己只管判断“这是否是预订请求”。

注意:工具函数的错误处理,比主流程更重要。务必用try-except包裹所有外部调用,并将错误信息转化为Agent能理解的JSON格式。例如{"error": "会议室A101在该时段已被预订,请选择其他时间"}。这样Agent才能基于错误信息,向用户提出有效备选方案。

3.3 状态管理:不是存聊天记录,是维护“任务进度图谱”

新手常把状态管理等同于“保存聊天历史”。但Agent的状态,远比对话记录复杂得多。它需要跟踪:当前任务树的执行节点、各分支的中间结果、用户隐含的偏好、未满足的依赖条件。比如一个“规划旅行行程”的Agent,状态可能包括:

  • 主任务:生成7天京都行程
  • 子任务1:查询5月京都樱花期(已完成,结果:4月10日-20日)
  • 子任务2:筛选符合预算的酒店(进行中,已排除3家)
  • 用户偏好:拒绝连锁酒店,倾向传统町屋
  • 依赖条件:需等待子任务2结果,才能开始子任务3(规划每日路线)

这种结构化状态,用简单的messages.append()根本无法承载。实操中,我推荐采用“轻量级状态机”模式,用Python字典定义状态结构:

class AgentState: def __init__(self): self.task_tree = { "root": {"status": "running", "result": None}, "subtask_1": {"status": "completed", "result": {"bloom_period": "2024-04-10 to 2024-04-20"}}, "subtask_2": {"status": "running", "result": []} } self.user_preferences = {"hotel_type": "machiya", "budget_max": 20000} self.dependencies = {"subtask_3": ["subtask_1", "subtask_2"]} def update_subtask(self, subtask_id: str, status: str, result=None): self.task_tree[subtask_id]["status"] = status if result is not None: self.task_tree[subtask_id]["result"] = result def can_run(self, subtask_id: str) -> bool: deps = self.dependencies.get(subtask_id, []) return all(self.task_tree[d]["status"] == "completed" for d in deps)

这个设计的优势在于:状态变更可审计、可预测、可回滚。每次调用update_subtask(),你都知道确切改变了哪个字段;每次判断can_run(),你都能清晰看到依赖关系。相比直接序列化整个messages列表,这种结构化状态让调试变得极其简单——当Agent卡在某步时,你直接print(state.task_tree)就能看到所有子任务的实时状态,而不是在几百行聊天记录里大海捞针。

更关键的是,这种状态设计天然支持“中断续办”。用户中途问“酒店选好了吗?”,Agent能立即从state.task_tree["subtask_2"]["result"]里提取已筛选的酒店列表,无需重新执行整个流程。我在某教育项目中应用此模式后,多轮对话的上下文准确率从63%提升至91%,因为Agent不再“失忆”,而是始终带着一张动态更新的“任务进度图谱”。

4. 实操过程与核心环节实现:手把手带你跑通第一个可交付Agent

4.1 第一步:搭建“思考-行动-观察”最小闭环(30分钟)

我们以“自动汇总今日科技新闻”为实战目标,全程不依赖任何框架,只用requests和openai(或国内某平台SDK)。目标:输入“汇总今日AI领域新闻”,输出包含3条新闻标题、来源、摘要的JSON。

Step 1:选择新闻API
推荐使用免费的NewsAPI(https://newsapi.org/),注册后获取API Key。它支持按日期、类别筛选,响应结构清晰。注意:免费版有速率限制(100次/天),够学习用。

Step 2:手动实现“行动”环节(工具函数)

import requests import json from datetime import datetime def fetch_ai_news() -> list: """ 获取今日AI领域新闻,返回结构化列表 """ today = datetime.now().strftime("%Y-%m-%d") url = f"https://newsapi.org/v2/everything?q=artificial+intelligence&from={today}&to={today}&sortBy=publishedAt&apiKey=YOUR_API_KEY" try: response = requests.get(url, timeout=10) response.raise_for_status() # 抛出HTTP错误 data = response.json() # 结构化提取(关键!避免后续解析失败) articles = [] for item in data.get("articles", [])[:3]: # 只取前3条 articles.append({ "title": item.get("title", "N/A"), "source": item.get("source", {}).get("name", "N/A"), "summary": item.get("description", "N/A")[:100] + "..." # 截断防超长 }) return articles except requests.exceptions.RequestException as e: return [{"error": f"News API request failed: {str(e)}"}] except Exception as e: return [{"error": f"News parsing failed: {str(e)}"}] # 测试工具函数 if __name__ == "__main__": print(json.dumps(fetch_ai_news(), indent=2, ensure_ascii=False))

实操心得:这里try-except的粒度很重要。我把网络请求和JSON解析分开捕获,因为前者是外部服务问题,后者是数据结构问题,修复策略完全不同。另外,[:100]截断是防止大模型处理超长文本,这是新手常忽略的性能细节。

Step 3:设计“思考”环节的Prompt(system + user)

SYSTEM_PROMPT = """你是一个专业的科技新闻编辑,负责将原始新闻数据整理成简洁、准确的摘要。所有输出必须是JSON格式,包含一个'news_summary'字段,其值为包含3个对象的列表。每个对象必须有'title'、'source'、'summary'三个字符串字段。不添加任何额外字段或解释。""" USER_PROMPT_TEMPLATE = """以下是今日AI领域的新闻数据: {news_data} 请严格按以下JSON格式输出摘要: {{ "news_summary": [ {{ "title": "新闻标题1", "source": "来源1", "summary": "摘要1(不超过100字)" }}, ... ] }}"""

Step 4:组装“观察”环节(调用大模型)

import openai def generate_summary(news_data: list) -> dict: """ 将新闻数据喂给大模型,生成结构化摘要 """ news_str = json.dumps(news_data, ensure_ascii=False, indent=2) user_prompt = USER_PROMPT_TEMPLATE.format(news_data=news_str) try: response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_prompt} ], temperature=0.3, # 降低随机性,保证格式稳定 max_tokens=500 ) # 解析模型输出(关键!必须处理可能的非JSON响应) raw_output = response.choices[0].message.content.strip() if raw_output.startswith("```json"): raw_output = raw_output[7:-3].strip() # 去除代码块标记 return json.loads(raw_output) except json.JSONDecodeError as e: print(f"JSON解析失败: {e}, 原始输出: {raw_output}") return {"error": "模型输出格式错误,请重试"} except Exception as e: return {"error": f"大模型调用失败: {str(e)}"} # 运行完整闭环 if __name__ == "__main__": news = fetch_ai_news() print("【工具调用结果】", news) summary = generate_summary(news) print("【最终输出】", json.dumps(summary, indent=2, ensure_ascii=False))

运行后,你会看到一个真正的、可交付的新闻汇总Agent。它不炫酷,但每一步都透明可控。这个闭环的价值在于:你亲手触摸了Agent的每一根神经——从API请求的timeout设置,到Prompt里temperature=0.3的取舍,再到JSON解析的容错处理。这种“亲手造轮子”的体验,是任何框架教程都无法替代的。

4.2 第二步:加入“反思”能力,让Agent学会自我纠错(2小时)

上一步的Agent有个硬伤:如果NewsAPI返回空数据,它只会输出{"error": "News API request failed..."},然后结束。但真实场景中,它应该尝试备选方案,比如换关键词搜索,或告知用户“今日暂无AI新闻,是否查看机器学习领域?”这就是“反思”(Reflection)环节。

我们通过在Prompt中加入“错误处理协议”来实现:

REFLECTION_SYSTEM_PROMPT = """你是一个具备自我反思能力的新闻编辑。当收到错误信息时,不要直接返回错误,而是: 1. 分析错误原因(是API不可用?还是无数据?) 2. 提出1个具体、可执行的备选方案 3. 用JSON格式输出,包含'error_analysis'、'alternative_action'、'suggested_query'三个字段 示例: { "error_analysis": "News API返回空结果,可能因当日无AI相关新闻", "alternative_action": "切换至机器学习领域搜索", "suggested_query": "机器学习" }""" def handle_error(error_msg: str) -> dict: """ 当fetch_ai_news或generate_summary返回error时,触发反思 """ try: response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": REFLECTION_SYSTEM_PROMPT}, {"role": "user", "content": f"错误信息:{error_msg}"} ], temperature=0.1, max_tokens=200 ) raw_output = response.choices[0].message.content.strip() if raw_output.startswith("```json"): raw_output = raw_output[7:-3].strip() return json.loads(raw_output) except Exception as e: return {"error_analysis": "反思模块故障", "alternative_action": "重试", "suggested_query": ""} # 在主流程中加入错误处理 def run_news_agent(): news = fetch_ai_news() if isinstance(news, list) and len(news) > 0 and "error" not in news[0]: return generate_summary(news) else: # 触发反思 reflection = handle_error(str(news)) print("【反思结果】", reflection) # 这里可以基于reflection['suggested_query']发起新搜索... return reflection

这个改进让Agent从“单次执行”升级为“任务流”。它不再是被动响应,而是主动诊断、主动决策。我在某金融项目中应用类似逻辑后,Agent的任务完成率从72%提升至89%,因为很多失败不是因为能力不足,而是缺乏“换个思路试试”的灵活性。

实操心得:反思Prompt的示例(example)必须真实。我最初用虚构示例,模型经常生成“联系管理员”这种无效方案。换成真实错误日志后,它才学会区分“网络超时”和“数据为空”的不同应对策略。这印证了前面说的:few-shot的质量,远胜于数量。

4.3 第三步:用状态机串联多任务,构建真实工作流(半天)

现在,我们把单任务Agent升级为多任务Agent:输入“帮我规划下周技术分享会”,它要依次完成:1)查下周可用会议室;2)查团队成员下周空闲时间;3)综合推荐3个候选时段。这需要状态机协调。

Step 1:定义状态结构(复用前面的AgentState)

class TechEventState: def __init__(self): self.tasks = { "find_rooms": {"status": "pending", "result": []}, "check_availability": {"status": "pending", "result": {}}, "generate_slots": {"status": "pending", "result": []} } self.context = { "event_purpose": "技术分享会", "attendees": ["张三", "李四", "王五"], "duration_hours": 2 } def next_pending_task(self) -> str: """按依赖顺序返回下一个待执行任务""" order = ["find_rooms", "check_availability", "generate_slots"] for task in order: if self.tasks[task]["status"] == "pending": return task return None

Step 2:编写任务执行器(Task Executor)

def execute_task(state: TechEventState, task_name: str) -> TechEventState: """ 根据任务名执行对应逻辑 """ if task_name == "find_rooms": rooms = find_available_rooms() # 伪代码,实际调用会议室API state.tasks["find_rooms"] = {"status": "completed", "result": rooms} elif task_name == "check_availability": # 基于state.tasks["find_rooms"]["result"]和state.context["attendees"] avail = check_team_availability(state.tasks["find_rooms"]["result"], state.context["attendees"]) state.tasks["check_availability"] = {"status": "completed", "result": avail} elif task_name == "generate_slots": slots = generate_time_slots( state.tasks["find_rooms"]["result"], state.tasks["check_availability"]["result"], state.context["duration_hours"] ) state.tasks["generate_slots"] = {"status": "completed", "result": slots} return state # 主循环:状态机驱动 def run_event_planner(): state = TechEventState() while True: next_task = state.next_pending_task() if not next_task: break print(f"执行任务: {next_task}") state = execute_task(state, next_task) # 输出最终结果 final_result = { "recommended_slots": state.tasks["generate_slots"]["result"], "used_rooms": state.tasks["find_rooms"]["result"][:3], "confirmed_attendees": len(state.context["attendees"]) } return final_result

这个设计的精妙之处在于:任务执行逻辑与状态流转逻辑完全解耦。execute_task()只关心“这个任务怎么做”,TechEventState只关心“现在该做什么”。这种分离让代码极易维护——新增一个“发送日程邀请”任务,只需在execute_task()里加一个elif分支,状态机自动识别并调度。我在某企业内部系统中用此模式,将原本3000行的硬编码工作流,重构为不到500行的可配置状态机,后续新增审批环节只改了20行代码。

注意:状态机的next_pending_task()方法,必须定义清晰的执行顺序。实际项目中,我会用DAG(有向无环图)替代线性顺序,支持并行任务(如“查会议室”和“查人员空闲”可同时进行)。但对新手,线性顺序足够建立概念。

5. 常见问题与排查技巧实录:那些没人告诉你的“血泪教训”

5.1 “模型不按Prompt格式输出”——不是模型问题,是你的Prompt没锁死

现象:明明写了“输出JSON格式”,模型却回复“好的,这是您要的摘要:{...}”,开头多了废话。

根本原因:大模型的“遵循指令”能力,高度依赖Prompt的结构强度。松散的指令(如“请用JSON格式”)会被忽略,而强约束指令(如“必须输出且仅输出JSON,不带任何前缀、后缀、解释”)才能生效。

独家排查技巧:

  • 加“格式锚点”:在Prompt末尾加一行--- OUTPUT START ---,并在解析时只取这行之后的内容。实测可提升格式合规率40%。
  • 用代码块标记强制:在system prompt里写“请将最终答案放在json和之间”,然后解析时只取代码块内内容。这是最简单有效的方案。
  • 温度值必须设为0.0-0.3:高于0.5时,模型会“发挥创意”,故意不守格式。这不是bug,是它的设计特性。

避坑心得:我曾为某政务项目写一个“政策解读Agent”,反复调试一周都没解决格式问题。最后发现,是system prompt里有一句“请用通俗易懂的语言解释”,这句话激活了模型的“解释欲”,导致它在JSON前加了一段说明。删掉这句话,问题立刻消失。所以,对格式敏感的Agent,system prompt里禁止出现任何“解释”“说明”“阐述”类动词。

5.2 “工具调用总是失败”——90%的情况,是参数没对齐

现象:book_meeting_room(room_id="A101", start_time="明天15:00")报错KeyError: 'start_time'。

真相:不是代码写错了,是API文档和你的认知有gap。比如某会议室API实际要求start_time是Unix时间戳,而你传了字符串;或者要求room_id必须是数字ID,而你传了"A101"。

系统化排查表:

排查步骤操作方法为什么有效
1. 查原始API文档打开API官网,找到book_meeting_room的POST请求示例,逐字比对字段名、类型、是否必填90%的失败源于文档没读全,比如忽略了timezone是必填参数
2. 抓包验证用Postman或curl,完全复现你的参数调用,看返回结果绕过所有代码层,直接验证API本身是否正常
3. 日志打点在工具函数开头加print(f"调用参数: {locals()}"),确认传入值很多时候你以为传了start_time,实际是

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询