从动态路由到单一模型:构建稳定LLM智能体的工程实践
如果你正在构建一个基于大语言模型(LLM)的智能应用,比如一个能联网搜索、处理文档、调用API的智能助手,那么你一定遇到过这个核心难题:如何让一个“大脑”(LLM)去灵活、可靠地调用各种“工具”(Tools)?
传统的解决方案,比如 LangChain 的 Agent 框架,其设计哲学很像一个动态路由网络。它让 LLM 在每次思考时,都像一个路由器一样,根据当前请求实时决定下一步该调用哪个工具。这听起来很智能,但实际开发中,开发者常常被其复杂性、不稳定的输出和难以调试的特性所困扰。
最近,一个名为Manifest的 AI 应用开发平台,做出了一个看似“倒退”,实则极具启发性的决策:他们宣布弃用自家基于动态路由的 LLM 路由器架构,转而拥抱“单一模型”策略。这个决定背后,是对当前 LLM 应用开发范式的深刻反思。
简单来说,Manifest 认为:与其让 LLM 费劲地扮演一个不稳定的“决策路由器”,不如让它专心做好“执行者”。把复杂的路由逻辑(什么时候、用什么工具、输入什么参数)从模型的实时推理中剥离出来,交给更确定、更可控的工程化方案去处理。
这篇文章,我们就来深入拆解这个技术决策。你将了解到:
- 动态路由(Agent)的理想与现实差距究竟在哪里?
- 单一模型(Single Model)策略如何工作,它真的更简单有效吗?
- 从 Manifest 的实践出发,我们该如何重新设计自己的 LLM 应用架构?
- 提供可落地的代码示例,展示如何用“函数调用(Function Calling)”这种“单一模型”思路,构建一个稳定可靠的智能体。
这不是一篇空谈趋势的文章。我们将从具体的技术痛点出发,通过对比和实操,让你彻底明白为什么在当前的模型能力下,“少即是多”的哲学可能才是构建生产级 AI 应用的关键。
1. 动态路由 Agent:理想很丰满,现实很骨感
在深入 Manifest 的新策略前,我们必须先理解它要替代的“旧方案”——动态路由 Agent。这几乎是过去一年里 LLM 应用开发的主流范式。
1.1 什么是动态路由 Agent?
你可以把它想象成一个由 LLM 驱动的高级任务调度器。其核心工作流程如下:
- 接收用户请求:例如,“帮我查一下北京明天天气,然后总结成一份出行建议报告。”
- LLM 思考与规划:模型分析请求,将其分解为子任务(1. 调用天气API;2. 调用文本总结能力)。
- 动态选择工具:模型从预设的工具箱(Toolkit)中,选择“天气查询”工具。
- 执行与观察:系统执行该工具,获取结果(“北京明天晴,15-25°C”),并将结果返回给 LLM。
- 循环决策:LLM 根据上一步的结果,决定下一步是继续调用其他工具(如“报告生成”),还是直接给出最终答案。
- 最终响应:LLM 综合所有中间结果,生成最终答案。
这个过程中,“选择工具”和“决定下一步”这两个关键决策点,是在每次循环中由 LLM 实时生成的。这就是“动态路由”——路径不是预设的,而是LLM根据上下文实时计算出来的。
1.2 动态路由的三大现实痛点
尽管这个设计非常符合人类“思考-行动-再思考”的直觉,但在工程落地时,问题接踵而至:
痛点一:输出不稳定与幻觉LLM 的输出具有随机性。它可能这次正确选择了get_weather工具,下次却莫名其妙地选择了一个不相关的send_email工具,或者生成不符合工具调用格式的文本。你需要编写复杂的输出解析器(Output Parser)和重试逻辑来兜底,代码复杂度急剧上升。
痛点二:调试与溯源困难当你的智能体行为异常时,你很难定位问题。是提示词(Prompt)没写对?是工具描述不够清晰?还是模型本身“抽风”了?你需要检查每一步的中间思考(Chain-of-Thought),这在大规模或并发请求下几乎是灾难。
痛点三:高昂的成本与延迟每一次“思考-行动”循环,都是一次完整的 LLM API 调用。一个复杂任务可能需要循环 5-10 次,这意味着成本是简单问答的 5-10 倍,总延迟也成倍增加。对于需要快速响应的生产环境,这是难以接受的。
痛点四:有限的可控性作为开发者,你很难对智能体的行为进行精确约束和引导。你无法轻易实现“优先使用A工具,失败后再尝试B工具”这样的确定性逻辑,一切依赖于模型的“临场发挥”。
正是这些痛点,促使像 Manifest 这样的平台开始寻找更优解。
2. Manifest 的转向:拥抱“单一模型”与确定性路由
Manifest 的核心理念可以概括为:将智能(Intelligence)与路由(Routing)解耦。
- 让 LLM 专注于它擅长的事:理解用户意图、处理自然语言、生成高质量文本、进行逻辑推理。
- 将路由逻辑交给确定性的程序代码:用
if-else、规则引擎、状态机或者更简单的分类模型来决定工具的使用流程。
2.1 “单一模型”策略如何工作?
这里的“单一模型”并非指只用一个模型,而是指在单次 LLM 交互中,完成所有必要的“智能”工作,工具调用流程则由外部逻辑控制。具体实现通常依赖于函数调用(Function Calling)或工具调用(Tool Calling)能力。
工作流程对比:
| 步骤 | 动态路由 (传统 Agent) | 单一模型 + 确定性路由 (新范式) |
|---|---|---|
| 1. 接收请求 | “查北京天气并写报告” | “查北京天气并写报告” |
| 2. 首次LLM交互 | LLM思考:需要先调用天气工具。 | LLM一次性分析:识别出需要两个功能:get_weather和generate_report。它直接返回一个结构化的调用列表或计划。 |
| 3. 路由决策 | 由LLM动态决定:调用get_weather。 | 由预设逻辑决定:程序收到LLM的分析结果,根据规则(如顺序执行)决定先执行get_weather。 |
| 4. 执行工具 | 执行get_weather(“北京”)。 | 执行get_weather(“北京”)。 |
| 5. 后续决策 | LLM再次思考:根据天气结果,决定调用报告工具。 | 程序控制:获取天气结果后,程序自动将结果作为输入,触发generate_report工具。 |
| 6. 最终响应 | LLM综合结果生成报告。 | LLM可能参与最终报告的润色,或由程序直接组装结果。 |
关键区别:在右侧的新范式中,“何时调用何工具”这个路由决策,从 LLM 的循环思考中剥离了出来,变成了程序代码可以控制的确定性过程。LLM 更像一个“规划师”或“识别器”,在开始时提供一次性的蓝图,后续由可靠的“施工队”(程序)按图施工。
2.2 为什么这样做更优?
- 稳定性极大提升:路由逻辑由代码控制,只要LLM的初始识别足够准确,后续流程就是100%确定的,不存在随机性。
- 调试变得简单:你可以清晰地看到LLM输出的“规划”(结构化数据),如果流程出错,你可以快速定位是“规划阶段”的识别问题,还是“执行阶段”的工具问题。
- 成本与性能可控:理想情况下,一次复杂任务只需要1-2次LLM调用(一次用于规划,一次用于最终合成),成本大幅降低,延迟也更可预测。
- 架构更清晰:智能体的业务逻辑变成了普通的程序代码,你可以方便地加入日志、监控、熔断、重试等成熟的工程化组件。
3. 实战:用“函数调用”构建一个稳定天气助手
理论说得再多,不如代码有说服力。下面我们用一个完整的例子,演示如何用 OpenAI 的“函数调用”功能(这是“单一模型”策略的典型实现)来构建一个天气查询助手。
我们将构建一个流程:
- 用户输入自然语言请求。
- LLM 识别意图,并返回符合格式的函数调用请求。
- 我们的程序根据LLM的指示,确定性地调用相应的天气API。
- 将API结果返回给LLM,让其生成友好的回复。
3.1 环境准备
你需要准备:
- Python 3.8+
- OpenAI API Key(我们将使用 GPT-3.5-turbo 或 GPT-4)
- 一个免费的天气API(这里用 Open-Meteo)
安装依赖:
pip install openai requests3.2 定义工具(函数)
首先,我们明确告诉LLM我们有什么工具可用。这通过一个“函数定义”的JSON列表来实现。
# tool_definitions.py # 定义可供LLM调用的“工具”(函数) WEATHER_FUNCTION = [ { "type": "function", "function": { "name": "get_current_weather", "description": "获取指定城市的当前天气情况", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "城市名称,例如:北京,上海,San Francisco", }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "温度单位,默认为摄氏度", } }, "required": ["location"], }, } } ]关键点:description和parameters的描述必须清晰准确,这直接决定了LLM能否正确识别和填充参数。
3.3 实现工具的执行函数
这是实际执行工作的代码,与LLM无关。
# weather_tool.py import requests def get_current_weather(location: str, unit: str = "celsius") -> str: """ 实际调用天气API的函数。 参数: location: 城市名 unit: 温度单位 返回: 天气信息的字符串描述 """ # 使用 Open-Meteo 免费天气API url = "https://api.open-meteo.com/v1/forecast" params = { "latitude": 39.9042, # 这里简化处理,实际应根据location查询经纬度 "longitude": 116.4074, "current_weather": True, "timezone": "auto" } try: response = requests.get(url, params=params) response.raise_for_status() data = response.json() current = data.get("current_weather", {}) temperature = current.get("temperature") weather_code = current.get("weathercode") # 简化天气代码转换 weather_map = { 0: "晴", 1: "晴间多云", 2: "多云", 3: "阴天", 45: "雾", 61: "小雨" } weather_desc = weather_map.get(weather_code, "未知") result = f"{location}的当前天气:{weather_desc},温度 {temperature}°C。" if unit == "fahrenheit": f_temp = temperature * 9/5 + 32 result = f"{location}的当前天气:{weather_desc},温度 {f_temp:.1f}°F。" return result except Exception as e: return f"获取天气信息失败:{str(e)}"3.4 核心流程:LLM识别 + 确定性执行
这是新范式的核心。我们只让LLM做一次“识别和规划”,然后由我们的代码接管流程。
# main.py import json from openai import OpenAI from tool_definitions import WEATHER_FUNCTION from weather_tool import get_current_weather # 初始化OpenAI客户端 client = OpenAI(api_key="你的OpenAI API Key") def run_conversation(user_query: str): """ 主流程:处理用户查询。 1. 发送查询和工具定义给LLM。 2. LLM返回是否调用工具及参数。 3. 程序根据LLM的指示,确定性地执行工具。 4. 将工具结果返回给LLM生成最终回复。 """ # 步骤1: 第一次LLM调用,让其识别意图 response = client.chat.completions.create( model="gpt-3.5-turbo-1106", # 或 gpt-4-turbo-preview,支持函数调用 messages=[{"role": "user", "content": user_query}], tools=WEATHER_FUNCTION, # 关键:告诉LLM可用的工具 tool_choice="auto", # 让LLM自动决定是否调用工具 ) message = response.choices[0].message print(f"LLM初始回复: {message}") # 步骤2: 检查LLM是否决定调用函数 if message.tool_calls: # 注意:这里可能有多个工具调用,我们按顺序处理 for tool_call in message.tool_calls: function_name = tool_call.function.name function_args = json.loads(tool_call.function.arguments) print(f"LLM决定调用工具: {function_name}") print(f"调用参数: {function_args}") # 步骤3: 确定性路由与执行 # 这里就是我们的“确定性路由逻辑”,用简单的if-else或字典映射 if function_name == "get_current_weather": location = function_args.get("location") unit = function_args.get("unit", "celsius") # 调用实际的功能函数 tool_response = get_current_weather(location, unit) print(f"工具执行结果: {tool_response}") # 步骤4: 将工具执行结果作为上下文,再次发送给LLM second_response = client.chat.completions.create( model="gpt-3.5-turbo-1106", messages=[ {"role": "user", "content": user_query}, message, # 包含LLM之前决定调用工具的消息 { "role": "tool", "tool_call_id": tool_call.id, "content": tool_response, # 工具执行结果 } ], # 这次不需要再传递tools,除非需要继续调用 ) final_message = second_response.choices[0].message print(f"\n最终回复: {final_message.content}") return final_message.content else: # 如果LLM认为不需要调用工具,直接回复 print(f"LLM直接回复: {message.content}") return message.content if __name__ == "__main__": # 测试用例 queries = [ "今天北京天气怎么样?", "帮我写一首关于春天的诗。", # 这个请求不会触发天气工具 "上海和北京的天气对比一下。" ] for query in queries: print(f"\n{'='*50}") print(f"用户查询: {query}") print(f"{'='*50}") result = run_conversation(query) print(f"最终答案: {result}")3.5 运行结果与解析
运行上述main.py,你会看到类似以下的输出:
================================================== 用户查询: 今天北京天气怎么样? ================================================== LLM初始回复: ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_abc123', function=Function(arguments='{"location": "北京", "unit": "celsius"}', name='get_current_weather'), type='function')]) LLM决定调用工具: get_current_weather 调用参数: {'location': '北京', 'unit': 'celsius'} 工具执行结果: 北京的当前天气:晴,温度 18.5°C。 最终回复: 北京现在是晴天,气温大约18.5摄氏度,天气不错哦! 最终答案: 北京现在是晴天,气温大约18.5摄氏度,天气不错哦!================================================== 用户查询: 帮我写一首关于春天的诗。 ================================================== LLM初始回复: ChatCompletionMessage(content='当然,这是一首关于春天的小诗:\n\n春风轻拂面,...', role='assistant', function_call=None, tool_calls=None) LLM直接回复: 当然,这是一首关于春天的小诗:... 最终答案: 当然,这是一首关于春天的小诗:...流程解析:
- 对于天气查询,LLM在第一次响应时,没有直接生成答案,而是返回了一个结构化的
tool_calls对象,指明了要调用的函数名和参数。 - 我们的程序检测到
tool_calls后,根据function.name进行确定性的路由(这里是一个简单的if判断),然后执行对应的get_current_weather函数。 - 获取天气结果后,程序将结果和原始对话历史一起,再次发送给LLM,由LLM生成一个自然、友好的最终回复。
- 对于写诗的请求,LLM判断无需调用工具,直接生成回复,流程结束。
这就是“单一模型”策略的精髓:LLM只负责“识别意图并结构化输出”,而“根据结构执行哪个函数”这个路由逻辑,完全由开发者编写的、确定性的代码控制。
4. 对比与进阶:从简单函数调用到复杂工作流
上面的例子是单工具调用。对于更复杂的任务(如“查天气并写报告”),我们可以让LLM在第一次调用时就返回一个完整的计划(Plan),然后由我们的程序来按序执行这个计划。
4.1 多步骤任务规划示例
我们可以定义一个create_plan的函数,让LLM生成一个任务列表。
# 扩展工具定义,增加报告生成函数 MULTI_FUNCTIONS = [ { "type": "function", "function": { "name": "create_plan", "description": "将复杂的用户请求分解为可执行的任务步骤序列", "parameters": { "type": "object", "properties": { "tasks": { "type": "array", "items": { "type": "object", "properties": { "step": {"type": "number"}, "action": {"type": "string"}, "tool": {"type": "string", "enum": ["get_weather", "generate_report", "search_web"]}, "parameters": {"type": "object"} }, "required": ["step", "action", "tool"] } } }, "required": ["tasks"] } } }, # ... 其他工具定义 (get_weather, generate_report) ] # 在主流程中,首先调用 create_plan # 获取计划后,程序遍历 tasks 列表,根据每个 task.tool 字段,确定性地调用对应的函数。 # 这完全脱离了LLM的动态决策循环。在这种架构下,LLM 只在最开始扮演一次“规划师”,后续所有步骤都是程序按部就班地执行。这带来了无与伦比的可控性和可观测性。
5. 常见问题与排查思路
在实践这种模式时,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| LLM不返回工具调用 | 1. 用户请求意图不明确。 2. 工具描述( description)不清晰。3. 模型温度(temperature)参数过高,导致输出不稳定。 | 1. 检查LLM的原始回复内容。 2. 简化并精确化工具描述。 3. 将 temperature设为0。 | 1. 优化用户Prompt,或先让LLM澄清意图。 2. 重写工具描述,确保无歧义。 3. 使用 temperature=0确保确定性。 |
| 工具调用参数错误 | 1. 参数定义(parameters)的schema不准确。2. LLM对用户输入的理解有偏差。 | 1. 打印出function_args检查。2. 在工具函数内部添加参数验证和日志。 | 1. 严格遵循JSON Schema规范定义参数。 2. 在调用实际工具前,对参数进行清洗和转换。 |
| 多工具调用顺序混乱 | 程序逻辑没有正确处理tool_calls列表的顺序。 | 打印整个tool_calls列表,观察其结构。 | 按照tool_calls列表的顺序依次执行,或根据业务逻辑(如依赖关系)重新排序。 |
| 流程无法处理失败 | 工具执行失败(如API超时)后,流程中断。 | 查看异常堆栈信息。 | 在每个工具调用外添加try-catch,并设计重试或降级逻辑。将失败信息反馈给LLM或用户。 |
6. 最佳实践与工程建议
基于 Manifest 的启示和上述实践,以下是在生产环境中应用“单一模型+确定性路由”策略的建议:
- 清晰定义工具边界:每个工具函数应职责单一,描述精确。避免让LLM去猜测一个模糊工具的作用。
- 强化输入验证:在调用实际工具前,务必验证LLM提供的参数。类型、范围、必填项都需要检查,防止无效调用。
- 实施结构化日志:记录每一次LLM的请求和响应(特别是
tool_calls)、每一次工具调用的输入输出。这是调试和监控的基石。 - 设计降级方案:当LLM无法识别意图或工具调用失败时,要有备选方案。例如,可以回退到直接问答,或提示用户重新表述。
- 控制成本与延迟:
- 对于复杂任务,优先使用一次“规划”调用+多次廉价工具执行的模式,而不是多次昂贵的LLM调用。
- 考虑对工具结果进行缓存,避免重复调用。
- 版本化管理提示词与工具定义:将工具的描述(Prompt)和函数的实现都纳入版本控制。它们的任何改动都可能显著影响智能体行为。
- 区分环境:在开发环境使用更便宜的模型(如 GPT-3.5-Turbo)进行流程测试,在上线前再用强模型(如 GPT-4)进行验证。
7. 总结:回归工程本质,让LLM做它该做的事
Manifest 放弃动态路由 LLM 路由器,选择“单一模型”策略,不是一个技术上的退步,而是一次面向工程现实的理性回归。
它告诉我们,在现阶段,LLM 最强大的能力依然是语言理解和生成,而不是复杂的实时决策与流程控制。试图让LLM承担后者,往往会将系统的不确定性和复杂性放大到难以管理的地步。
作为开发者,我们的职责是构建可靠、可维护、可预测的系统。将确定性的路由逻辑从LLM中抽离,用代码去实现,正是这种工程思维的体现。LLM 应该被当作一个强大的“语义理解与规划组件”嵌入到我们的系统中,而不是成为整个系统不可控的“大脑”。
这种架构转变,降低了AI应用开发的门槛,提高了系统的稳定性和可观测性,使得AI能力能够更可靠地集成到生产流程中。下次当你设计自己的LLM应用时,不妨先问自己:“这个决策逻辑,真的需要LLM来实时判断吗?能不能让它一次性告诉我该怎么做,然后由我来执行?”
或许,这就是构建下一代可靠AI智能体的关键起点。