从零构建LangChain智能体:核心架构、代码实现与生产级优化指南
1. 项目概述从零构建智能体理解LangChain-Agents的核心脉络最近在AI应用开发圈子里一个叫“agents-from-scratch”的项目热度挺高。它不是一个全新的框架而是LangChain-AI官方推出的一个开源教程项目。说白了这就是一份“手把手教你造轮子”的深度指南。它的目标非常明确不是让你直接调用AgentExecutor这个黑盒而是带你从最基础的原理出发用最纯粹的Python代码一步步搭建起一个能思考、能使用工具、能自主完成复杂任务的智能体Agent。为什么这件事值得花时间现在大语言模型LLM的应用开发很多都停留在简单的问答或者文档总结上。一旦遇到需要多步骤推理、调用外部工具比如搜索、计算、查数据库、并且根据中间结果动态调整策略的复杂任务一个简单的chain.invoke()就搞不定了。这时候就需要智能体。但市面上的框架往往把智能体的复杂性封装了起来你只知道输入和输出中间那个“思考-行动-观察”的循环ReAct Loop对你来说就是个谜。这个项目就是要打破这个黑盒。它适合谁呢首先肯定是那些不满足于仅仅当个“API调用工程师”的开发者。你想深入理解智能体是如何做决策的它的思考过程Thought、行动Action、观察Observation是如何流转的。其次是那些在业务中遇到了智能体“失控”情况的团队。比如智能体陷入了死循环或者总是选择错误的工具你想调试却无从下手。通过从零构建你能清晰地看到每一个判断逻辑知道在哪里“埋点”来监控和修正它的行为。最后它也适合教学和学习。对于想进入AI应用层开发的新手来说这是一个绝佳的、低门槛的实践入口能帮你建立起对智能体架构最扎实的认知。这个项目的价值就在于它剥离了所有花哨的包装直指智能体最核心的“大脑”与“手脚”如何协同工作的本质。接下来我们就深入这个“大脑”的内部看看它的设计思路是如何一步步成型的。2. 核心架构设计拆解智能体的“大脑”与“手脚”要理解如何从零构建一个智能体我们得先抛开LangChain那些高级的Toolkit和AgentType回到最本质的问题一个能自主完成任务的程序它需要哪些基本组件这个项目的设计思路清晰地勾勒出了这几个核心部分一个负责“思考”的大语言模型LLM、一套可供调用的“工具”Tools、一个解析LLM输出并驱动循环的“执行引擎”Execution Engine、以及一套定义交互规则的“输出解析器”Output Parser。2.1 大脑大语言模型LLM的角色与约束在这个架构里LLM是绝对的核心是智能体的“大脑”。但它在这里扮演的角色和我们在简单聊天中使用的LLM有本质区别。在智能体中LLM不是一个自由的对话者而是一个被严格约束的“推理引擎”。它的核心任务是根据当前的用户目标Goal和已有的历史信息包括之前的思考、行动和观察结果生成下一步的“计划”。这里的关键约束体现在提示词工程上。你不能简单地问模型“接下来怎么办”。你必须给它一个高度结构化的指令模板这个模板通常遵循ReActReasoning and Acting范式。一个经典的模板会要求模型严格按照以下格式输出Thought: 我需要分析当前情况。用户想了解天气但我没有实时信息所以我需要使用搜索工具。 Action: Search Action Input: {query: 北京今天天气}这个模板强制LLM的输出结构化将“思考”和“行动”分开。Thought部分是模型的内部推理帮助我们理解它的决策过程这对调试至关重要。Action和Action Input则是可被程序解析的明确指令告诉执行引擎该调用哪个工具以及传入什么参数。注意选择LLM时并非越强大越好。你需要考虑模型的“指令遵循能力”。一些较小的、经过指令微调的模型如Qwen2.5-7B-Instruct在严格遵循输出格式方面有时比更大的、但未经严格约束的通用模型表现更稳定。成本、速度和稳定性是需要权衡的因素。2.2 手脚工具Tools的抽象与封装工具是智能体的“手脚”是它感知和影响外部世界的唯一途径。一个工具本质上是一个函数它有明确的名称、描述、参数列表和实现逻辑。项目的设计精髓在于对工具的抽象。一个典型的工具接口会包含name: 工具的唯一标识符LLM将通过这个名字来调用它。description: 对工具功能的自然语言描述。这部分至关重要因为LLM完全依靠这段描述来理解该工具能做什么、在什么场景下使用。描述必须清晰、准确、无歧义。args_schema: 定义工具输入参数的类型如Pydantic模型这为LLM生成结构化的Action Input提供了依据。_run方法工具的实际执行逻辑。例如一个搜索工具的描述可能是“一个用于在互联网上搜索最新信息的工具。当问题涉及实时事件、未知事实或需要最新数据时使用此工具。输入应为搜索查询字符串。” 而一个计算器的描述则是“一个用于执行数学计算的工具。当问题涉及算术、代数或数值计算时使用此工具。输入应为数学表达式字符串。”工具的设计原则是“单一职责”和“良好描述”。避免创建功能过于复杂或描述模糊的工具这会导致LLM困惑不知道何时该调用它。2.3 循环引擎执行与状态管理这是整个智能体的“中枢神经系统”它负责驱动著名的“思考-行动-观察”循环。它的工作流程是一个典型的控制循环初始化接收用户查询和工具列表初始化对话历史通常包含系统提示词和用户问题。循环开始将当前对话历史和工具描述组合成完整的提示词发送给LLM。解析输出使用Output Parser解析LLM的回复期望得到结构化的(thought, action, action_input)或一个表示最终答案的final_answer。判断终止如果解析出final_answer循环结束返回答案。执行行动如果解析出action则根据action名称找到对应的工具并以action_input为参数调用该工具的_run方法。观察结果将工具执行的结果observation格式化为字符串例如Observation: 北京今天晴气温25-32摄氏度。并追加到对话历史中。迭代回到第2步开始下一轮循环。这个引擎还必须处理异常情况比如LLM输出的格式不符合预期、指定的工具不存在、工具执行出错等。一个健壮的引擎需要包含错误处理和重试机制例如当解析失败时可以给LLM一个错误提示并要求它重新输出。2.4 输出解析器与LLM的契约输出解析器是连接非结构化LLM文本输出和结构化程序逻辑的桥梁。它定义了程序期望LLM遵守的“通信协议”。在从零构建时我们通常实现一个基于正则表达式或关键字匹配的解析器。它的核心逻辑是扫描LLM返回的文本寻找如Thought:、Action:、Action Input:、Final Answer:这样的关键词。一旦匹配到Action就需要进一步解析其后的内容通常是工具名并尝试解析Action Input通常是一个JSON字符串或纯文本。如果匹配到Final Answer则提取其后的内容作为最终结果并终止循环。实操心得输出解析是智能体不稳定的主要来源之一。LLM偶尔会“创造性”地输出一些变体比如用中文“思考”代替“Thought:”或者在JSON外多加几个引号。一个技巧是在系统提示词中强烈强调输出格式并可以在解析器中加入一些容错逻辑比如忽略大小写、匹配多种可能的关键词前缀。但最好的方法还是通过高质量的提示词工程和模型选择来从根本上减少格式错误。3. 从零实现核心循环一行代码一行代码地构建理解了架构我们现在动手用最直观的代码把上述概念实现出来。我们会创建一个简单的智能体它拥有两个工具一个搜索工具模拟和一个计算器工具。3.1 第一步定义工具基类与具体工具首先我们定义一个所有工具的基类它规定了工具的接口。from abc import ABC, abstractmethod from typing import Any, Dict from pydantic import BaseModel, Field class Tool(ABC): 工具基类 name: str description: str args_schema: type[BaseModel] None abstractmethod def _run(self, *args, **kwargs) - str: 工具的执行逻辑返回字符串格式的观察结果 pass def run(self, tool_input: str) - str: 对外提供的运行接口可在此添加日志、验证等逻辑 # 这里可以添加输入验证如果定义了args_schema return self._run(tool_input)接下来我们实现两个具体的工具。为了简化搜索工具我们用一个模拟函数代替真实的网络请求。class SearchTool(Tool): name search description 一个用于在互联网上搜索最新信息的工具。当问题涉及实时事件、未知事实或需要最新数据时使用此工具。输入应为搜索查询字符串。 def _run(self, query: str) - str: # 模拟搜索返回结果 mock_database { 北京今天天气: 北京2023年10月27日天气晴朗气温10-18摄氏度西北风2-3级。, LangChain是什么: LangChain是一个用于开发由大语言模型驱动的应用程序的框架。, 圆周率: 圆周率是圆的周长与直径的比值约等于3.14159。 } result mock_database.get(query, f未找到关于{query}的信息。) return result class CalculatorTool(Tool): name calculator description 一个用于执行基本数学计算的工具。当问题涉及算术、代数或数值计算时使用此工具。输入应为数学表达式字符串如 3 5 * 2。 def _run(self, expression: str) - str: try: # 警告在生产环境中直接使用eval是极其危险的容易导致代码注入。 # 这里仅用于演示。实际应用应使用安全的表达式求值库如 ast.literal_eval 或自定义解析器。 result eval(expression) return str(result) except Exception as e: return f计算错误{e}重要安全警告上述计算器工具中的eval()函数仅用于最简单的演示绝对不可用于任何生产环境或暴露给不可信的用户输入。恶意用户可以通过输入__import__(os).system(rm -rf /)这样的字符串来执行任意系统命令造成灾难性后果。在实际项目中必须使用安全的替代方案如只处理数字和运算符的解析器或使用ast.literal_eval但它也只支持字面量不支持函数调用。3.2 第二步构建提示词模板提示词是指导LLM行为的“剧本”。我们需要一个模板将工具描述、历史对话和当前问题组合起来。from typing import List def build_prompt(question: str, tools: List[Tool], history: List[str]) - str: 构建智能体的提示词。 # 工具描述部分 tools_text \n.join([f{tool.name}: {tool.description} for tool in tools]) # 历史对话部分 history_text \n.join(history) if history else # 完整的提示词模板 prompt_template f 你是一个智能助手可以使用以下工具 {tools_text} 你必须严格按照以下格式回应 Thought: 首先你需要思考当前的情况和可用的工具决定下一步该做什么。 Action: 你将要执行的动作必须是以下工具之一[{, .join([t.name for t in tools])}] Action Input: 执行动作所需的输入通常是一个字符串。 Final Answer: 当你认为已经获得了足够的信息来回答用户的问题时用这个字段给出最终答案。 开始 {history_text} Question: {question} return prompt_template.strip()这个模板清晰地告诉LLM三件事你能用什么工具列表、你应该怎么回答严格的格式、以及当前上下文历史和问题。3.3 第三步实现输出解析器我们需要一个函数来解析LLM的回复将其转化为程序可以理解的结构。import re from typing import Tuple, Optional def parse_llm_output(text: str) - Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]: 解析LLM的输出。 返回: (thought, action, action_input, final_answer) 如果解析失败返回 (None, None, None, None) thought action action_input final_answer None # 使用正则表达式匹配关键字段允许字段后跟冒号或中文冒号等变体 thought_match re.search(rThought\s*[:]\s*(.*?)(?\n\s*(Action|Final Answer)|$), text, re.DOTALL | re.IGNORECASE) action_match re.search(rAction\s*[:]\s*(\w), text, re.IGNORECASE) action_input_match re.search(rAction Input\s*[:]\s*(.*?)(?\n\s*(Thought|Final Answer)|$), text, re.DOTALL | re.IGNORECASE) final_answer_match re.search(rFinal Answer\s*[:]\s*(.*?)$, text, re.DOTALL | re.IGNORECASE) if thought_match: thought thought_match.group(1).strip() if action_match: action action_match.group(1).strip() if action_input_match: action_input action_input_match.group(1).strip().strip(\) # 去除可能的引号 if final_answer_match: final_answer final_answer_match.group(1).strip() # 如果找到了最终答案则返回它 if final_answer is not None: return thought, None, None, final_answer # 否则返回动作相关信息 return thought, action, action_input, None这个解析器相对健壮能处理一些简单的格式变化。但在生产环境中可能需要更复杂的逻辑比如尝试用json.loads解析Action Input如果失败再回退到字符串。3.4 第四步组装执行引擎现在我们把所有零件组装起来形成主循环。class SimpleAgent: def __init__(self, llm_client, tools: List[Tool], max_iterations: int 10): 初始化智能体。 :param llm_client: 一个可调用的LLM客户端接收字符串提示词返回字符串回复。 :param tools: 可用工具列表。 :param max_iterations: 最大循环次数防止无限循环。 self.llm llm_client self.tools {tool.name: tool for tool in tools} self.max_iterations max_iterations def run(self, question: str) - str: 运行智能体处理一个问题 history [] # 存储对话历史Thought, Action, Observation for i in range(self.max_iterations): # 1. 构建当前轮次的提示词 prompt build_prompt(question, list(self.tools.values()), history) print(f\n--- 第 {i1} 轮迭代 ---) print(f提示词发送给LLM:\n{prompt[:500]}...) # 打印前500字符 # 2. 调用LLM try: llm_response self.llm(prompt) print(fLLM原始回复:\n{llm_response}) except Exception as e: return f调用LLM时出错{e} # 3. 解析LLM回复 thought, action, action_input, final_answer parse_llm_output(llm_response) # 4. 判断是否为最终答案 if final_answer is not None: print(f智能体给出了最终答案: {final_answer}) return final_answer # 5. 检查解析出的动作是否有效 if not action or action not in self.tools: error_msg f解析失败或工具{action}不存在。LLM回复格式可能不正确。 print(error_msg) # 将错误信息作为观察加入历史让LLM在下轮修正 history.append(fThought: {thought or N/A}) history.append(fAction: {action or N/A}) history.append(fObservation: {error_msg}) continue # 继续下一轮循环 # 6. 执行工具调用 print(f智能体决定: Thought: {thought}) print(f Action: {action}, Input: {action_input}) try: tool self.tools[action] observation tool.run(action_input) print(f工具执行结果: {observation}) except Exception as e: observation f执行工具{action}时出错{e} print(observation) # 7. 将本轮信息加入历史用于下一轮思考 history.append(fThought: {thought}) history.append(fAction: {action}) history.append(fAction Input: {action_input}) history.append(fObservation: {observation}) # 循环超过最大次数仍未得出答案 return f经过{self.max_iterations}轮尝试仍未解决问题。可能问题太复杂或需要其他工具。3.5 第五步模拟LLM客户端并运行测试为了演示我们创建一个模拟的LLM客户端。它不会真的调用API而是根据我们的提示词返回预设的、符合格式的回复。这能让我们清晰地跟踪逻辑。def mock_llm_client(prompt: str) - str: 一个模拟的LLM客户端。 在实际应用中这里应替换为对OpenAI、Anthropic、本地模型等API的调用。 # 这是一个非常简单的规则模拟器仅用于演示循环逻辑。 # 它“识别”问题中的关键词来做出反应。 if 天气 in prompt: return Thought: 用户想了解天气信息这是一个需要实时数据的问题。我应该使用搜索工具。 Action: search Action Input: 北京今天天气 elif 计算 in prompt or 加上 in prompt: return Thought: 用户提出了一个数学计算问题。我应该使用计算器工具。 Action: calculator Action Input: 3 5 * 2 elif LangChain in prompt: # 假设经过一轮搜索后历史中包含了搜索结果LLM现在可以给出最终答案 # 这里模拟LLM在收到Observation后决定给出最终答案 if Observation: in prompt and LangChain是一个用于开发 in prompt: return Thought: 我已经通过搜索工具获取了关于LangChain的信息现在可以回答用户的问题了。 Final Answer: LangChain是一个用于开发由大语言模型驱动的应用程序的框架。 else: return Thought: 用户想了解LangChain这是一个事实性问题我需要搜索一下。 Action: search Action Input: LangChain是什么 else: # 默认回复表示不知道 return Thought: 我不确定如何处理这个问题也没有合适的工具。我直接给出一个诚实的回答吧。 Final Answer: 我目前无法回答这个问题。 # 初始化工具和智能体 tools [SearchTool(), CalculatorTool()] agent SimpleAgent(llm_clientmock_llm_client, toolstools, max_iterations5) # 测试1简单查询 print(*50) print(测试1查询天气) result1 agent.run(北京今天天气怎么样) print(f最终结果: {result1}) print(\n *50) print(测试2数学计算) result2 agent.run(请帮我计算3加上5乘以2等于多少) print(f最终结果: {result2}) print(\n *50) print(测试3多轮对话需要搜索后回答) result3 agent.run(LangChain是什么) print(f最终结果: {result3})运行这段代码你会看到控制台打印出完整的执行流程每一轮的提示词、LLM的回复、解析出的动作、工具执行结果以及最终答案。这个过程透明地展示了智能体内部的“思考-行动-观察”循环是如何一步步推进的。通过这个从零构建的示例智能体不再是神秘的黑盒它的每一个决策、每一次调用都清晰可见。这为我们后续的调试、优化和功能扩展打下了坚实的基础。4. 进阶优化与生产级考量我们构建了一个可以运行的基础智能体但它离一个健壮、高效、可用于实际项目的生产级系统还有很大距离。从“玩具”到“工具”我们需要在多个层面进行加固和优化。这部分将深入探讨那些在官方教程或简单Demo中不会提及但在真实场景中至关重要的实践细节。4.1 提示词工程的精雕细琢基础模板能跑通流程但要让智能体表现得更聪明、更稳定提示词需要精心设计。1. 少样本示例Few-Shot Examples在系统提示词中直接提供几个正确输入输出的例子是引导LLM遵循格式和理解任务最有效的方法之一。例如在工具描述部分之后可以加入以下是几个示例 Question: 旧金山现在的温度是多少 Thought: 用户询问实时温度我需要搜索天气信息。 Action: search Action Input: 旧金山当前温度 Question: 计算15的平方加上20除以4的结果。 Thought: 这是一个数学计算问题。 Action: calculator Action Input: 15**2 20/4 Question: 谁写了《百年孤独》 Thought: 这是一个关于书籍作者的事实性问题我可以直接给出答案如果我知道或者使用搜索工具确认。我知道答案所以直接回答。 Final Answer: 《百年孤独》的作者是加夫列尔·加西亚·马尔克斯。这些示例能显著降低LLM输出格式错误的概率并教会它在什么情况下选择直接回答什么情况下使用工具。2. 思维链Chain-of-Thought强化鼓励LLM进行更细致的推理。可以在Thought部分的要求中更具体“你需要逐步推理首先分析问题的核心需求然后检查已有知识和历史信息接着评估可用工具中哪个最适合最后决定行动或给出答案。” 这能促使模型输出更逻辑化的思考过程不仅便于调试有时也能提升最终决策的质量。3. 工具描述的优化工具描述不能太长但必须包含关键信息功能、适用场景、输入格式、输出示例。避免使用模糊的词语。对比以下两种描述较差“一个搜索工具。”太模糊较好“用于获取当前事件、最新新闻或未知事实信息的网络搜索工具。输入应为简洁的搜索查询关键词如‘2024年奥运会举办地’。输出为相关的文本摘要。”4. 历史长度管理与总结随着对话轮次增加提示词会越来越长可能触及模型的上下文窗口限制且无关历史会干扰当前决策。一个高级技巧是动态历史管理不是简单地将所有Thought/Action/Observation都堆进去而是定期进行总结。例如每3轮或当历史token数超过阈值时让LLM自己或用一个更小的模型对之前的交互进行摘要然后用一句总结性的话如“之前我们已经搜索了北京的天气得知是晴天。”替换掉冗长的原始历史记录。这能极大地提升长对话任务的效率和质量。4.2 健壮性加固错误处理与循环控制基础循环非常脆弱我们需要为它穿上“盔甲”。1. 输出解析的容错与重试我们的基础解析器使用正则表达式但LLM的输出可能有各种意外情况。我们需要一个更强大的解析策略多模式解析首先尝试用正则匹配标准格式。如果失败尝试查找类似“我想我应该用计算器算一下”这样的自然语言并映射到工具名这需要一个小型的意图分类器或关键词映射表。JSON模式优先在提示词中强烈要求Action Input必须是合法的JSON字符串。解析时先用json.loads()尝试解析这比正则更可靠。解析失败重试当解析失败时不要直接崩溃或进入错误状态。可以将解析失败的原始文本和错误信息连同一条新的指令如“你的回复格式不正确请严格按照要求的‘Thought/Action/Action Input/Final Answer’格式重新回答。”一起作为新的提示词发送给LLM。通常设置1-2次重试就能纠正大部分格式错误。2. 工具执行异常处理工具执行可能因网络、权限、输入无效等原因失败。SimpleAgent中简单的try-except是第一步。更进一步我们应该对不同类型的异常进行分类处理可重试错误如网络超时等待后自动重试该工具调用。输入错误如计算器收到非数学表达式将清晰的错误信息如“计算器工具期望一个数学表达式但收到了‘你好世界’”作为Observation返回给LLM让它修正输入。工具致命错误记录日志并返回一个通用错误信息同时可以考虑在后续轮次中暂时禁用该工具防止智能体反复撞墙。3. 循环逃生机制除了最大迭代次数还需要更智能的停止条件重复动作检测记录最近N次执行的动作如果检测到完全相同的(Action, Action Input)组合在循环很可能陷入了死循环。此时应终止并返回错误。无进展检测如果连续几轮Observation的内容都高度相似或没有带来新的关键信息可以判断智能体“卡住了”需要外部干预或终止。用户中断实现一个机制允许在长时间运行后询问用户是否继续或接收用户的“停止”指令。4.3 性能与可观测性当智能体处理真实、复杂的任务时性能和监控变得至关重要。1. 异步执行如果智能体需要调用多个不相互依赖的工具或者工具本身是I/O密集型如网络请求同步顺序执行会造成大量等待时间。可以将工具调用改为异步。例如在解析出Action后使用asyncio.create_task()来并发执行工具。但要注意这改变了“思考-行动-观察”的顺序变成了“思考-并发行动-观察所有结果”适用于行动间无依赖的场景。对于有依赖的场景仍需顺序执行。2. 流式输出与中间思考展示对于需要长时间运行的任务向用户实时展示智能体的“思考过程”Thought和Observation能极大提升用户体验和信任感。这需要支持流式响应。你可以将LLM的调用改为流式模式逐步获取Thought文本并推送给前端。同样工具执行的关键状态也可以实时更新。3. 全面的日志与追踪生产系统必须拥有完善的日志。记录每一轮的完整提示词、LLM响应、解析结果、工具调用详情输入、输出、耗时、最终答案。这不仅是调试的黄金资料也是分析智能体行为模式、发现常见错误、优化提示词和工具集的基础。可以考虑集成像LangSmith这样的追踪平台它能可视化整个智能体的调用链方便进行根因分析。4. 工具路由优化当工具数量很多几十上百个时让LLM从一长串描述中选择工具会变得低效且容易出错。此时可以引入工具路由层。先用一个快速、廉价的分类模型或一个小型LLM对用户问题进行分析预筛选出最相关的3-5个工具再将这个缩小的工具集和描述提供给主LLM进行决策。这能减少主LLM的负担提升准确率和速度。4.4 扩展性设计智能体的“进化”一个设计良好的智能体架构应该易于扩展。1. 工具的动态注册与管理不应在代码中硬编码工具列表。可以设计一个工具注册表允许在运行时动态添加、移除或禁用工具。这对于构建插件系统或让智能体能力随时间增长至关重要。2. 支持复杂参数与结构化输入我们的示例中Action Input是字符串。但很多工具需要复杂的结构化参数。可以通过在args_schema中定义Pydantic模型并在提示词中明确描述参数的JSON结构引导LLM生成合法的JSON。在解析Action Input后先用Pydantic模型验证和解析再传递给工具执行。3. 多智能体协作对于极其复杂的任务可以引入“多智能体系统”。例如一个“规划智能体”负责将大任务分解成子任务多个“执行智能体”分别拥有不同的工具集来处理子任务一个“协调智能体”负责汇总结果和决策。这相当于将我们构建的单个智能体作为基础模块在更高层级上进行组织和编排。从零构建这样的系统挑战巨大但理解单个智能体的运作是第一步。通过以上这些进阶考量我们看到了将一个教学演示级别的智能体打磨成一个能在实际业务中承担责任的可靠组件的完整路径。每一步优化都源于真实场景中遇到的挑战其目的是让智能体更聪明、更健壮、更高效。这个过程没有终点需要开发者根据具体业务需求持续迭代和平衡。5. 常见陷阱、调试技巧与实战心得即使有了清晰的架构和健壮的代码在实际操作中你依然会碰到各种光怪陆离的问题。这部分分享的就是那些在文档里找不到只有亲手构建和调试过多个智能体后才能积累下的“血泪经验”。掌握这些能让你在遇到问题时快速定位而不是对着莫名其妙的输出干瞪眼。5.1 智能体行为异常诊断清单当智能体表现不如预期时可以按照以下清单自上而下进行排查问题现象可能原因排查步骤与解决方案LLM不调用工具直接给出答案1. 工具描述不清晰或缺乏吸引力。2. 提示词中未强调“必须使用工具”。3. LLM本身“偷懒”已知问题。1.检查工具描述确保描述明确指出了使用场景如“当问题涉及实时信息时…”。2.强化指令在系统提示词开头使用强硬语气如“你必须使用提供的工具来回答问题。在拥有足够信息前禁止直接给出最终答案。”3.提供反例在Few-Shot示例中加入一个“未使用工具导致答案错误”的例子。LLM总是调用同一个错误工具1. 工具描述相似度太高LLM无法区分。2. 某个工具的描述过于“通用”或“有吸引力”。1.差异化描述重写工具描述突出其独特性和边界。例如区分“搜索新闻”和“搜索学术论文”明确各自领域。2.调整描述顺序将更具体、更可能被用到的工具放在列表前面。LLM有时会对列表开头的工具有偏好。Action Input格式错误非JSON1. 提示词未要求JSON格式。2. LLM“忘记”了格式要求。1.明确要求JSON在Action Input的示例中明确展示JSON格式如Action Input: {query: 问题}。2.在解析前进行后处理写一个函数尝试将LLM输出的非JSON字符串如query: 问题修复成JSON{query: 问题}。智能体陷入死循环1. 工具返回的结果无法让LLM推进任务。2. LLM的思考逻辑出现循环。1.检查工具输出工具是否返回了“未找到”、“错误”等无信息结果改进工具使其在无结果时返回更有指导性的信息如“未找到相关信息请尝试更换关键词。”2.添加循环检测如第4.2节所述实现重复动作检测。在历史中看到相同模式时在Observation中明确警告LLM“你正在重复之前的操作这没有进展。请重新评估你的计划。”处理复杂、多步骤问题能力差1. LLM的上下文长度或推理能力有限。2. 缺乏任务分解的引导。1.引入子目标在提示词中教导LLM进行任务分解。例如“对于复杂问题你应该先制定一个分步计划。在Thought中先列出步骤123…”。2.使用更强的模型对于复杂逻辑考虑换用推理能力更强的模型如GPT-4、Claude 3。5.2 提示词调试的“显微镜”技巧提示词的微小改动可能对结果产生巨大影响。以下是行之有效的调试方法1. 记录与回放务必保存每一轮交互的完整提示词和完整LLM响应。当出现问题时不要只看最终输出要把这个“对话记录”拿出来仔细分析。问题往往出在某一轮特定的提示词或响应上。2. 人工扮演LLM这是最强大的调试技巧。当你对智能体的行为感到困惑时自己扮演LLM。拿着出现问题时的那一轮提示词包含所有历史和工具描述不要看智能体实际的输出你自己作为“人类LLM”严格按照格式要求写一个你认为“正确”的回复。然后对比你的回复和实际LLM的回复。如果你的回复也不同说明提示词本身有歧义或引导不足需要修改提示词。如果你的回复和“正确”逻辑一致但LLM的回复错了那问题可能出在模型能力或随机性上。可以考虑增加示例、降低温度参数、或换用其他模型。3. 温度参数的权衡temperature参数控制输出的随机性。对于需要严格遵循格式的智能体任务通常建议设置为0或一个很低的值如0.1以保证输出的确定性和一致性。较高的温度虽然能带来创造性但也会增加格式错误和不可预测行为的风险。5.3 工具设计的黄金法则工具的好坏直接决定了智能体的能力上限。1. 工具原子化一个工具只做一件事并且把它做好。不要设计一个“万能搜索”工具而是拆分成“搜索新闻”、“搜索百科”、“搜索代码仓库”等。原子化的工具描述更清晰LLM更容易准确调用也便于单独测试和优化。2. 输入输出标准化尽可能让所有工具的输入和输出格式保持简单和一致。例如输入尽量是单个字符串或简单的键值对JSON。输出必须是纯文本字符串并且包含足够的信息供LLM进行下一轮推理。避免输出复杂的HTML或嵌套JSON除非你愿意在工具内部或解析层做额外的格式化处理。3. 为失败设计工具不可能永远成功。在设计工具时就要考虑失败情况下的输出。输出不应该只是一个None或异常堆栈。应该返回一个对LLM友好的错误描述例如“搜索失败网络连接超时请稍后再试。” 或 “数据库查询错误提供的ID‘abc’格式无效。” 这能帮助LLM理解问题所在并可能采取纠正措施如重试或换用其他工具。从零构建一个智能体的过程是一个不断与模型“对齐”、与不确定性“搏斗”、并将抽象逻辑“固化”为可靠代码的过程。它没有银弹每一个成功的智能体背后都充满了反复的调试、提示词的微调和工具集的打磨。但正是这种深度的参与让你能真正驾驭这项技术而不是被它牵着鼻子走。当你看到自己构建的智能体流畅地完成一个复杂任务时那种对系统每个环节都了如指掌的掌控感是直接调用高级API无法比拟的。这或许就是从零开始最大的意义。