1. 从“什么都交给大模型”说起:Agent 设计的核心矛盾
1.1 一个几乎所有人都踩过的坑
做 Agent 开发的人,大概率都经历过这样一个阶段:拿到需求,第一反应就是“把大模型接进来,让它自己判断、自己决策、自己调用工具”。提示词写得越来越长,工具描述越堆越多,System Prompt 从几百字膨胀到几千字,最后发现——模型开始“犯迷糊”了。
我最早做 Agent 项目的时候也是这样。一个客服场景的智能体,我恨不得把公司所有的业务规则、所有的工具接口、所有的边界条件全塞进 System Prompt 里,指望大模型能像一个全能员工一样,什么都能干、什么都干对。结果呢?简单问题回答得挺好,一旦涉及多步骤的工具调用,就开始出问题:该调 A 工具的时候调了 B,该传三个参数的时候只传了两个,该在第二步等结果的时候直接跳到了第四步。
这不是模型能力不够的问题。即便是当时最强的模型,在这种“什么都交给大模型”的架构下,稳定性也很难让人满意。后来我慢慢意识到,问题的根源不在于模型本身,而在于架构设计——我们把太多本该由确定性代码完成的工作,交给了概率性的模型去处理。
Jev 这个项目给出的新答案,核心就在这一点上:Agent 不应该是一个“大模型包办一切”的系统,而应该是一个“大模型负责它擅长的部分,其余交给确定性组件”的分层架构。
1.2 大模型在 Agent 里到底该干什么
要理解 Jev 的思路,先得想清楚一个问题:大模型在 Agent 系统里,真正的核心价值是什么?
我的理解是三个字:理解力。具体来说,是理解自然语言意图的能力、理解上下文语义的能力、以及在开放场景下做出合理判断的能力。这些是传统规则引擎和状态机做不到的,也是大模型不可替代的地方。
但大模型不擅长什么?精确计算、状态管理、流程控制、确定性执行。你让大模型去算一个复杂的数学表达式,它可能给你一个看起来很像但实际错误的结果;你让大模型去维护一个多轮对话的状态机,它可能在第三轮就忘了第一轮设定的约束条件;你让大模型去严格按照某个流程执行五步操作,它可能在第二步就“自作主张”地跳到了第五步。
所以 Jev 的设计哲学其实很朴素:让大模型做它擅长的事——理解意图、生成内容、处理开放性问题;让确定性代码做它擅长的事——流程控制、状态管理、工具调度、参数校验。两者通过清晰的接口协作,而不是把所有逻辑都塞进一个巨大的 Prompt 里。
这个思路听起来简单,但真正落地的时候,需要解决一系列具体问题:怎么划分边界?怎么设计接口?怎么保证大模型和确定性组件之间的通信可靠?下面我结合自己的实践经验,把这些细节一个个拆开来讲。
1.3 为什么这个话题现在值得认真聊
Agent 这个概念从 2023 年火到现在,市面上已经出现了大量的框架和工具。但如果你真正做过生产级的 Agent 项目,就会发现一个尴尬的现实:Demo 很惊艳,上线很艰难。原因有很多,但“过度依赖大模型”绝对是最核心的一个。
我见过太多项目,架构图看起来很漂亮——一个大模型节点连着十几个工具节点,箭头四通八达。但实际跑起来,错误率居高不下,排查问题极其困难,因为你根本不知道是哪一步出了问题:是模型理解错了?是工具调用参数传错了?还是流程控制逻辑有 bug?
Jev 提出的分层思路,本质上是在解决这个“不可观测、不可控制”的问题。当大模型只负责它该负责的那一层时,整个系统的行为就变得可预测、可调试、可优化了。这对于真正要把 Agent 落地到生产环境的团队来说,价值非常大。
2. Jev 的核心设计思路:分层与边界
2.1 System One Model 与 System Two 的分工逻辑
Jev 的设计里有一个很关键的概念,叫做System One Model。这个名字借用了心理学里“快思考”和“慢思考”的框架——System One 是直觉式的、快速的、自动化的反应;System Two 是理性的、慢速的、需要刻意控制的思考。
在 Jev 的架构里,大模型扮演的是 System One 的角色:快速理解用户意图,生成初步的响应或决策建议。而 System Two 的角色,则由确定性组件来承担:流程引擎、状态管理器、工具调度器、参数校验器等等。
为什么这么分?因为大模型的推理过程本质上是“黑盒”的,你无法精确控制它每一步在做什么。而 Agent 系统在生产环境中,恰恰需要精确控制——每一步调用了什么工具、传了什么参数、返回了什么结果、下一步该走哪个分支,这些都需要是可追踪、可复现的。
我自己的经验是,当一个 Agent 系统的核心流程由确定性代码控制时,调试效率会提升一个数量级。出了问题,你可以直接看日志:第几步调用了哪个工具,参数是什么,返回是什么,一目了然。而不是对着一堆模型输出的自然语言去猜“它到底想干什么”。
2.2 什么该交给大模型,什么不该
这是整个设计里最关键的问题。根据我的实践经验,可以按照下面这个原则来划分:
交给大模型的部分:
- 自然语言理解:把用户的模糊表达转化为结构化的意图
- 内容生成:需要创造性或开放性的文本输出
- 模糊匹配:在多个可能的选项中做出合理选择
- 异常处理:当确定性流程遇到未预期的情况时,由模型给出兜底方案
不交给大模型的部分:
- 流程控制:步骤的顺序、分支的判断、循环的终止
- 状态管理:多轮对话中的上下文状态、会话变量
- 工具调度:什么时候调用哪个工具、按什么顺序调用
- 参数校验:工具调用的参数是否合法、是否完整
- 结果处理:工具返回结果的解析、格式化、错误处理
这个划分不是绝对的,不同场景下边界会有调整。但核心原则是:凡是能用确定性代码可靠完成的事情,就不要交给大模型。大模型应该被用在那些“非它不可”的地方。
2.3 边界划分的实际操作:一个具体例子
光说原则可能还是有点抽象,我举一个实际项目中的例子来说明。
假设我们要做一个“会议安排助手”的 Agent,用户说:“帮我约一下张三和李四,下周三下午找个时间开个会,大概一个小时,讨论一下 Q3 的预算方案。”
如果按照“什么都交给大模型”的思路,你可能会写一个很长的 Prompt,告诉模型:你要先解析用户意图,然后查日历,然后找空闲时间,然后发邀请……模型确实可能完成这个任务,但稳定性堪忧。
按照 Jev 的分层思路,我会这样设计:
第一层:意图理解(大模型负责)模型的任务只有一个:把用户的自然语言转化为结构化的意图对象。比如输出一个 JSON:
{ "action": "schedule_meeting", "participants": ["张三", "李四"], "time_range": "下周三下午", "duration_minutes": 60, "topic": "Q3 预算方案" }第二层:流程控制(确定性代码负责)拿到这个结构化意图后,后面的所有步骤都由代码控制:
- 调用日历 API 查询张三和李四下周三下午的空闲时间
- 计算交集,找到满足 60 分钟的时间段
- 如果有多个可选时间段,调用模型选择一个最合适的(这里模型又参与了一下)
- 调用日历 API 创建会议邀请
- 发送通知
第三层:结果生成(大模型负责)最后,把执行结果交给模型,让它生成一句自然的回复给用户。
这样设计的好处是:模型只在两个明确的点上参与,而且每次参与的任务都很聚焦。流程控制、状态管理、工具调用这些容易出错的环节,全部由确定性代码完成。整个系统的可靠性大幅提升。
3. 核心细节解析:Jev 架构的关键组件
3.1 意图解析层:让模型做“翻译”而不是“决策”
在 Jev 的架构里,大模型最核心的一个职责就是意图解析。但这里有一个关键区别:模型做的是“翻译”,不是“决策”。
翻译的意思是:把用户的自然语言表达,转化为结构化的、机器可理解的数据格式。比如用户说“帮我看看最近有什么新电影”,模型输出{"intent": "query_movies", "time_range": "recent", "sort_by": "release_date"}。这个过程不需要模型做任何决策,只需要它理解语言并做格式转换。
决策的意思是:模型自己判断该调用哪个工具、该走哪条分支、该用什么参数。这个过程充满了不确定性,模型可能做出合理但不一致的选择。
我自己的做法是,在意图解析层,给模型一个非常明确的输出格式约束(通常是 JSON Schema),并且只让它做翻译工作。如果用户的表达不够明确,模型可以输出一个“需要澄清”的意图,由确定性代码决定下一步是追问还是走默认逻辑。
这里有一个实操心得:意图解析的 Prompt 要尽可能短而精。很多人喜欢在意图解析的 Prompt 里塞大量业务规则,这是不对的。业务规则应该放在确定性代码里,意图解析的 Prompt 只需要告诉模型“把用户的话翻译成这个格式”就够了。Prompt 越短,模型的输出越稳定。
3.2 流程引擎:Agent 的“骨架”
流程引擎是 Jev 架构里最容易被忽视、但最重要的组件。它负责控制整个 Agent 的执行流程:先做什么、后做什么、什么条件下走哪个分支、什么时候结束。
为什么流程引擎不能交给大模型?因为流程控制需要的是确定性和可复现性。同样的输入,流程引擎每次都会走出同样的路径;而大模型每次的输出可能都不一样。在生产环境里,这种不确定性是致命的。
我通常会用状态机或者 DAG(有向无环图)来实现流程引擎。每个节点代表一个步骤,节点之间的边代表流转条件。流程引擎的执行是完全确定性的:给定当前状态和输入,下一步走到哪里是唯一确定的。
这样做还有一个好处:可观测性。每个节点的执行都可以打日志,你可以清楚地看到整个流程走了哪些节点、每个节点的输入输出是什么、在哪个节点出了错。排查问题的效率比“对着模型输出猜”高太多了。
3.3 工具调度器:精确控制每一次调用
工具调度器负责管理 Agent 可以使用的所有工具,以及每次工具调用的参数校验、执行、结果处理。
在“什么都交给大模型”的架构里,工具调用通常是这样的:把工具的描述放在 Prompt 里,让模型自己决定调哪个工具、传什么参数。这种方式的问题在于,模型可能选错工具、传错参数、或者在不该调用的时候调用。
Jev 的做法是:工具调度由确定性代码控制,模型只负责提供参数。具体来说,流程引擎决定在哪个步骤调用哪个工具,然后从当前上下文中提取参数(可能来自用户输入、可能来自之前的工具返回结果、也可能来自模型的输出),校验参数合法性,执行调用,处理返回结果。
这里有一个细节值得展开:参数校验非常重要。模型输出的参数经常会有格式问题——该传数字的传了字符串、该传数组的传了单个值、该传枚举值的传了一个不在枚举范围内的值。如果没有参数校验,这些错误会直接传到工具层,导致调用失败。有了参数校验,可以在调用之前就发现问题,并且可以设计重试逻辑:让模型重新生成参数,或者用默认值兜底。
3.4 状态管理器:多轮对话的“记忆”
多轮对话是 Agent 的常见场景,也是大模型最容易出问题的地方。模型在长对话中会“遗忘”早期的约束条件,或者把不同轮次的信息混淆。
Jev 的思路是:状态管理由确定性代码负责,模型只负责读取和写入状态。具体来说,状态管理器维护一个结构化的状态对象,包含会话变量、历史操作记录、当前流程位置等信息。每次调用模型时,把相关的状态信息作为上下文传进去;模型输出的结果,由状态管理器决定如何更新到状态中。
这样做的好处是,状态是显式的、结构化的、可检查的。你可以随时查看当前状态是什么,而不是依赖模型“记住”了什么。如果发现状态不对,也可以精确地定位是哪一步更新出了问题。
4. 实操过程:从零搭建一个分层 Agent
4.1 环境准备与技术选型
在开始搭建之前,先说一下技术选型。Jev 本身是一个开源项目,你可以直接使用它的框架,也可以参考它的设计思路自己实现。我这里以自己实现为例,讲一下需要准备的东西。
核心依赖:
- 一个大模型 API(可以是任何主流模型,根据你的场景选择)
- 一个流程引擎库(Python 的话可以用
transitions或者自己写一个简单的状态机) - 一个 JSON Schema 校验库(比如
jsonschema) - 一个日志框架(推荐
structlog,结构化日志对调试 Agent 非常有帮助)
项目结构建议:
agent/ ├── core/ │ ├── intent_parser.py # 意图解析层 │ ├── flow_engine.py # 流程引擎 │ ├── tool_dispatcher.py # 工具调度器 │ └── state_manager.py # 状态管理器 ├── tools/ │ ├── calendar.py # 日历工具 │ └── notification.py # 通知工具 ├── prompts/ │ └── intent_parsing.txt # 意图解析的 Prompt 模板 └── main.py # 入口这个结构的好处是职责清晰,每一层都可以独立测试和替换。比如你想换一个大模型,只需要改intent_parser.py里的 API 调用;你想加一个新工具,只需要在tools/下加一个文件,然后在tool_dispatcher.py里注册。
4.2 意图解析层的实现
先来看意图解析层的核心代码。这里的关键是定义一个清晰的输出 Schema,然后让模型按照这个 Schema 输出。
import json from jsonschema import validate, ValidationError INTENT_SCHEMA = { "type": "object", "properties": { "intent": {"type": "string", "enum": ["schedule_meeting", "query_schedule", "cancel_meeting", "unknown"]}, "parameters": { "type": "object", "properties": { "participants": {"type": "array", "items": {"type": "string"}}, "time_range": {"type": "string"}, "duration_minutes": {"type": "integer", "minimum": 15, "maximum": 480}, "topic": {"type": "string"} } }, "confidence": {"type": "number", "minimum": 0, "maximum": 1} }, "required": ["intent", "parameters", "confidence"] } def parse_intent(user_input: str, context: dict) -> dict: prompt = build_intent_prompt(user_input, context) raw_output = call_llm(prompt) try: parsed = json.loads(raw_output) validate(instance=parsed, schema=INTENT_SCHEMA) return parsed except (json.JSONDecodeError, ValidationError) as e: # 校验失败,触发重试或降级逻辑 return handle_parse_failure(user_input, e)这段代码有几个关键点值得说明:
第一,Schema 要尽可能严格。enum限制了 intent 的取值范围,minimum和maximum限制了 duration 的合理范围。Schema 越严格,模型输出不合法的概率越低。
第二,一定要做校验。不要假设模型一定会输出合法的 JSON。我自己的经验是,即使 Prompt 写得再好,模型也有大约 2%-5% 的概率输出格式有问题的结果。校验是必须的。
第三,要有降级逻辑。当解析失败时,不要直接报错给用户。可以设计一个降级策略:比如让模型重新生成一次,或者用一个更简单的规则引擎做兜底,或者直接告诉用户“我没理解您的意思,能再说一遍吗”。
4.3 流程引擎的实现
流程引擎我推荐用状态机来实现。下面是一个简化版的例子:
from enum import Enum from transitions import Machine class State(Enum): IDLE = "idle" PARSING = "parsing" CHECKING_AVAILABILITY = "checking_availability" SELECTING_TIME = "selecting_time" CREATING_EVENT = "creating_event" NOTIFYING = "notifying" DONE = "done" FAILED = "failed" class MeetingAgent: states = [s.value for s in State] def __init__(self): self.machine = Machine( model=self, states=self.states, initial=State.IDLE.value ) self.machine.add_transition("start", State.IDLE.value, State.PARSING.value) self.machine.add_transition("parse_success", State.PARSING.value, State.CHECKING_AVAILABILITY.value) self.machine.add_transition("parse_fail", State.PARSING.value, State.FAILED.value) self.machine.add_transition("availability_found", State.CHECKING_AVAILABILITY.value, State.SELECTING_TIME.value) self.machine.add_transition("no_availability", State.CHECKING_AVAILABILITY.value, State.FAILED.value) # ... 其他转换状态机的好处是,每个状态的转换条件都是显式定义的,不会出现“模型自己决定下一步”的情况。而且状态机的当前状态是明确可查的,调试的时候非常方便。
4.4 工具调度与参数校验
工具调度器的核心是参数校验和错误处理。下面是一个工具注册和调用的例子:
class ToolDispatcher: def __init__(self): self.tools = {} def register(self, name: str, func, param_schema: dict): self.tools[name] = {"func": func, "schema": param_schema} def call(self, name: str, params: dict) -> dict: if name not in self.tools: return {"success": False, "error": f"Unknown tool: {name}"} tool = self.tools[name] try: validate(instance=params, schema=tool["schema"]) except ValidationError as e: return {"success": False, "error": f"Invalid params: {e.message}"} try: result = tool["func"](**params) return {"success": True, "data": result} except Exception as e: return {"success": False, "error": str(e)}这里的关键是:工具调用永远返回一个结构化的结果对象,包含success字段和data或error。这样流程引擎可以根据success字段决定下一步走哪个分支,而不需要去解析工具返回的自然语言。
4.5 状态管理的实现
状态管理我通常用一个简单的字典加上版本控制来实现:
class StateManager: def __init__(self): self.state = {} self.history = [] def update(self, key: str, value): self.history.append({"key": key, "old": self.state.get(key), "new": value}) self.state[key] = value def get_context(self, keys: list = None) -> dict: if keys is None: return self.state.copy() return {k: self.state.get(k) for k in keys}每次调用模型时,通过get_context获取相关的状态信息,作为上下文传给模型。模型输出的结果,由流程引擎决定如何更新到状态中。这样状态的变化是完全可追踪的,出了问题可以回溯每一步的变更。
5. 常见问题与排查技巧实录
5.1 模型输出格式不稳定怎么办
这是最常见的问题。即使你定义了严格的 JSON Schema,模型还是可能输出格式有问题的结果。我的经验是,从三个方面来解决:
第一,Prompt 里给出明确的示例。不要只描述格式,要给一个完整的输入输出示例。模型对示例的遵循程度远高于对描述的理解。
第二,使用 JSON Mode 或 Function Calling。现在很多模型 API 都支持强制 JSON 输出,或者通过 Function Calling 来约束输出格式。如果可用,优先使用这些特性。
第三,设计重试和降级逻辑。第一次解析失败后,可以把错误信息反馈给模型,让它重新生成。如果连续失败两次,就降级到规则引擎或者直接告诉用户“我没理解”。
5.2 流程引擎的状态爆炸问题
当 Agent 的流程比较复杂时,状态机的状态数量可能会爆炸式增长。我遇到过一个项目,状态机有 30 多个状态,维护起来非常痛苦。
解决思路是分层状态机。把整个流程拆成几个大的阶段,每个阶段内部有自己的子状态机。阶段之间的转换由顶层状态机控制,阶段内部的转换由子状态机控制。这样每个状态机的复杂度都可控。
另一个思路是用 DAG 替代状态机。如果流程中没有循环,用 DAG 来表达会更清晰。每个节点是一个步骤,边是流转条件,整个流程一目了然。
5.3 工具调用超时和失败的处理
工具调用失败是常态,不是异常。网络抖动、API 限流、服务不可用,这些都可能发生。我的做法是:
- 每个工具调用都设置超时(通常 5-10 秒)
- 失败后自动重试 1-2 次,使用指数退避
- 重试仍然失败,返回结构化的错误信息给流程引擎
- 流程引擎根据错误类型决定是继续、跳过还是终止
这里有一个细节:不要把工具调用的错误信息直接交给模型去处理。模型可能会“编造”一个看起来合理的解释,而不是如实告诉用户“系统暂时不可用”。错误处理应该由确定性代码完成,模型只负责在最后生成用户友好的提示语。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 模型输出不是合法 JSON | Prompt 不够明确 / 模型能力不足 | 检查 Prompt 是否有示例 / 换模型测试 | 加示例、用 JSON Mode、加重试 |
| 工具调用参数错误 | Schema 定义不严格 / 模型理解偏差 | 打印模型原始输出和校验错误 | 收紧 Schema、加参数说明 |
| 流程卡在某个状态 | 转换条件不满足 / 工具调用超时 | 查看状态机日志和工具调用日志 | 加超时、加兜底转换 |
| 多轮对话状态丢失 | 状态未正确更新 / 上下文未传入 | 检查状态管理器的 history | 修复状态更新逻辑 |
| 模型“自作主张”跳过步骤 | 流程控制交给了模型 | 检查是否有 Prompt 让模型控制流程 | 把流程控制移到确定性代码 |
5.5 几个踩过的坑
坑一:Prompt 太长导致模型“注意力分散”。我早期喜欢把所有的规则都写进 System Prompt,结果模型经常忽略其中的某些规则。后来把 Prompt 拆成多个小的、聚焦的 Prompt,每个 Prompt 只做一件事,效果好了很多。
坑二:过度依赖模型的“常识”。有些边界条件,我以为模型能理解,但实际上它并不理解。比如“不要给同一个人重复发通知”,模型可能会觉得“发两次也没什么”。这种业务规则必须写在确定性代码里。
坑三:忽略了工具调用的幂等性。有一次工具调用超时了,我设计了重试逻辑,结果重试的时候创建了两个相同的会议。后来所有写操作的工具都加了幂等键,问题才解决。
坑四:日志不够结构化。早期用print打日志,出了问题根本没法排查。后来换成结构化日志,每个日志都带trace_id、step、input、output,排查效率提升了很多。
6. 这套思路适合什么场景,不适合什么场景
6.1 适合的场景
Jev 这套分层思路,最适合的是业务流程相对明确、对可靠性要求高的 Agent 场景。比如:
- 客服工单处理:流程固定,但需要理解用户的自然语言描述
- 日程安排助手:步骤明确,但需要解析模糊的时间表达
- 数据查询助手:查询逻辑确定,但需要理解用户的查询意图
- 审批流程助手:审批规则固定,但需要理解申请内容
这些场景的共同特点是:流程是确定的,但输入是开放的。确定性代码负责流程,大模型负责理解输入,两者各司其职。
6.2 不太适合的场景
反过来,如果你的 Agent 场景是高度开放、流程不固定的,那这套思路可能就不太适用。比如:
- 创意写作助手:没有固定流程,每一步都依赖模型的创造性
- 开放式研究助手:探索路径不固定,需要模型自主决策
- 复杂谈判 Agent:策略需要动态调整,很难预先定义流程
这些场景下,大模型需要更大的自主权,确定性代码能控制的部分比较少。但这不意味着 Jev 的思路完全没用——即使在开放场景下,你也可以把一些确定性的环节(比如工具调用、状态管理)抽出来,只把真正需要创造性的部分交给模型。
6.3 一个折中方案:混合架构
实际项目中,纯粹的“全确定性”和“全模型驱动”都很少见,更多的是混合架构。我的做法是:
- 核心流程用确定性代码控制,保证可靠性和可观测性
- 在关键决策点引入模型,让模型在有限的选项里做选择
- 在开放环节给模型更大自主权,但设置明确的边界和兜底逻辑
这样既保证了系统的可靠性,又保留了模型的灵活性。具体怎么划分,取决于你的业务场景和对可靠性的要求。
7. 我个人的一些实践体会
做 Agent 开发这几年,最大的体会就是:不要被“智能”这个词迷惑。一个 Agent 系统好不好用,不在于它用了多强的模型,而在于它的架构设计是否合理。大模型是一个强大的组件,但它只是一个组件,不是万能药。
Jev 给我的启发,与其说是具体的技术方案,不如说是一种设计哲学:把复杂系统拆解成可管理的部分,每个部分用最合适的工具去解决。大模型负责它擅长的理解和生成,确定性代码负责它擅长的控制和执行。两者通过清晰的接口协作,而不是混在一起。
这个思路不仅适用于 Agent 开发,其实适用于很多系统设计场景。当你发现一个系统变得难以调试、难以维护的时候,往往就是因为不同职责的组件混在了一起。把它们拆开,让每个部分做自己最擅长的事,问题往往就迎刃而解了。
最后分享一个我最近在用的调试技巧:给 Agent 的每一步都打上 trace_id,然后把整个执行链路可视化出来。当你看到一次完整的执行流程——从用户输入,到意图解析,到流程流转,到工具调用,到最终输出——每一步的输入输出都清清楚楚的时候,排查问题就变成了一件很轻松的事。这个习惯帮我省了大量的调试时间,推荐你也试试。