Agent架构设计:大模型与确定性代码的分层协作实践 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 很惊艳上线很艰难。原因有很多但“过度依赖大模型”绝对是最核心的一个。我见过太多项目架构图看起来很漂亮——一个大模型节点连着十几个工具节点箭头四通八达。但实际跑起来错误率居高不下排查问题极其困难因为你根本不知道是哪一步出了问题是模型理解错了是工具调用参数传错了还是流程控制逻辑有 bugJev 提出的分层思路本质上是在解决这个“不可观测、不可控制”的问题。当大模型只负责它该负责的那一层时整个系统的行为就变得可预测、可调试、可优化了。这对于真正要把 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. 实操过程从零搭建一个分层 Agent4.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(instanceparsed, schemaINTENT_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( modelself, statesself.states, initialState.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: fUnknown tool: {name}} tool self.tools[name] try: validate(instanceparams, schematool[schema]) except ValidationError as e: return {success: False, error: fInvalid 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 常见问题速查表问题现象可能原因排查方向解决方案模型输出不是合法 JSONPrompt 不够明确 / 模型能力不足检查 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然后把整个执行链路可视化出来。当你看到一次完整的执行流程——从用户输入到意图解析到流程流转到工具调用到最终输出——每一步的输入输出都清清楚楚的时候排查问题就变成了一件很轻松的事。这个习惯帮我省了大量的调试时间推荐你也试试。