三个月前,我接手了一个“改造Agent”的活儿:公司现有的客服Agent在通用对话上还算机灵,但一碰到具体业务流程就露馅;另一个团队做的业务自动化脚本倒是把退款、改地址、催发货做得明明白白,可稍微换一种问法就死机。两边都想融合,结果每次融合都变成一场灾难——通用模型把业务规则当“闲聊”给带偏了,业务代码又不断干扰模型的自由发挥。这不是个别现象,几乎每个做Agent落地的团队都会撞上这堵墙:通用智能和业务深度,到底该怎么放进同一个系统里?
这篇文章想跟你聊一个我自己验证有效的设计思路:把Agent的通用推理能力和业务领域知识看成两个“正交”的维度,在架构上彻底解耦。你不需要是算法专家,只要会看代码、有基本的系统设计概念,就能理解下面这套做法。我会用一个真实项目(订单售后场景)从零搭一个“正交化Agent”,讲清楚每一步为什么这么做,并把我踩过的坑和排查经验全部整理出来。如果你正在做Agent开发,或者正准备启动一个Agent项目,这篇东西应该能帮你少走很多弯路。
1. 当“通用智能”撞上“业务深度”:一场每天都在发生的架构拉扯
1.1 我踩过的两个极端:要么是“什么都懂”的聊天机器人,要么是“只会一件事”的死板流程
先说说我见过最多的两种失败形态。
第一种,大家把Agent当作一个“万能大脑”。一个大模型,把所有业务知识、行业术语、公司流程全塞进System Prompt里,然后期待它既懂常识又懂业务。试过的人都知道结果:它在闲聊测试里表现完美,一旦用户说出“我上个月买的那双鞋,左脚的鞋带孔有毛刺,想换货,但我原包装盒扔了”,它就不知道怎么结合售后规则来判断能不能换。不是模型能力不行,而是通用推理链被大量业务细节给稀释了,模型分不清哪些是“常识推理”,哪些是“强制规则”。
第二种,大家把Agent当作一个“定时脚本”。用传统编程把业务流程写死,每一步都由代码控制,模型只负责填几个槽位。这种系统的好处是稳定,但代价是用户不能有一点偏离脚本:同一个意思换个说法就识别不了,跨场景的组合请求更是完全无能为力。这个月是这个流程,下个月业务变了,你改的不是一个参数,而是一整条代码逻辑链。
我之所以一直强调这是“架构问题”而不是“模型问题”,是因为底层模型已经很强了,问题是它被架错了位置。你让一个擅长逻辑推理的大脑去背一万条业务规则,它当然会累;你让一条精确的业务流水线去应对开放对话,它当然会僵。
1.2 为什么说“正交”是这个问题的最优解:像正交编码器一样互不干扰又精确联动
“正交”这个词在不同地方有不同含义。在数学里,正交矩阵的特点是行向量之间彼此独立、内积为零;在信号处理里,正交IQ锁相解调靠两路相位差90度的本振信号,把微弱的目标信号从强干扰里稳稳捞出来;在工业控制里,正交编码器用A/B两路相位差90度的脉冲判断旋转方向和位置,我在一个电机项目里用过这玩意儿,A相和B相哪怕有一根线接反,你都得调半天。这些应用都有一个共同点:两个维度互不污染,但又配合得极其精确。
Agent开发里的“正交”,我把它定义为:通用智能负责“怎么想”,业务深度负责“知道该做什么”。两者不是竞争关系,而是两个正交的坐标轴。通用智能管推理、分解任务、理解语义、生成回复;业务深度管领域规则、流程约束、数据格式、校验逻辑。通用层不需要知道“七天无理由退货要满足什么条件”,业务层也不需要知道“用户这句话的意图有哪三种可能性”。
关键在于,这两个轴必须在架构上各自独立,而不是混在一个层级里。如果业务规则散落在提示词、Agent逻辑、外部工具三处,那系统改一次业务就要动一次通用层,这就破坏了正交性。反过来,如果通用推理能力放进业务代码里硬编码,那换一个底层模型就得重写整个业务层。
真正稳定的Agent架构,应该让通用层像一个“操作系统”,业务层像运行在它上面的“应用”。你想升级模型、换推理策略,业务App不用动;你想调整退款规则、新增物流查询功能,通用内核也不用动。这个思想听起来简单,实际操作起来坑很多,后面我会一步步展开。
2. 正交化Agent设计的核心拆解:让通用推理与领域知识各归其位
2.1 内核只做三件事:感知、决策、行动
先忘掉“别人家的Agent框架”,回到本质。一个Agent内核,无论外面包装得多玄乎,核心循环就是三件事:感知(Perception)、决策(Planning/Decision)、行动(Action)。
感知负责把外部的输入(用户消息、系统事件、文件内容)统一成内部可理解的格式。决策负责判断“现在要解决什么问题、需要哪些信息、分几步做”。行动负责执行具体操作,调用工具、查数据库、调用子Agent、生成回复。
正交化的第一原则是:这三件事必须保持纯粹,不能因为业务不同而改变。不管你在做客服Agent还是运维Agent,感知应该用同一套语义理解模块,决策应该用同一套任务分解策略,行动应该用同一个工具调用协议。“业务深度”不能长在内核里,否则这个内核就退化成专用脚本了。
我在一个项目里试过,把“订单状态判断”直接写进决策逻辑,结果是这个Agent只能做客服。后来想扩展一个“工单风险预警”功能,决策层全乱套了,因为业务逻辑和推理逻辑已经缠成一团。正确做法是:决策层只负责“我决定调用一个技能”,至于这个技能里是不是查订单表,那是业务层的事。
2.2 业务深度不塞进Prompt,而是外挂成“技能”和“工具”
既然业务深度不能进内核,那它应该放在哪?我的实践答案是:放在Agent外面的“技能层”,通过标准接口被内核调用。
要理解这个设计,可以先想清楚“Skill”和“Agent”到底有什么区别。一个Agent是一个可以独立感知-决策-行动的完整智能体;一个Skill则是一个能力包,它知道“某个具体领域怎么做”。Agent可以拥有多个Skill,然后根据任务动态挑选合适的Skill来执行。这就像你是一个项目经理(Agent),手下有法务专员、财务专员、技术专员(Skill),需要处理什么问题,就调动哪类专员。你要是把法务细则全背在自己脑子里,其他项目就没法干了。
具体的实现上,我会把“业务深度”拆成两个载体:
- Skill:偏重“流程判断”的业务能力。比如“售后退款Skill”知道退款申请应该按什么条件审批,知道不同支付渠道的到账时限,能够输出“可以退款”“需要人工复核”等结论。
- Tool:偏重“操作”的能力。比如“查订单Tool”接收一个订单号就返回订单状态,“发短信Tool”接收手机号和内容就完成发送。
Agent内核通过统一的接口调用Skill和Tool,但不知道它们内部是怎么实现的。这样带来的一个直接好处是:你可以用不同的底层模型来跑同一个逻辑技能,今天用本地部署的Hermes Agent,明天切到云端模型,Skill接口不变,业务代码一行都不用改。
如果你的项目比较小,不一定要区分“技能”和“工具”。但一旦业务逻辑复杂到需要多步判断,强烈建议分开,否则工具注册表会变得臃肿,意图识别也会跟着混乱。
2.3 记忆与上下文管理的隔离设计
很多Agent跑着跑着就“人格分裂”,原因大多出在上下文管理上。你让通用模型记住“用户是VIP、当前订单编号、退款状态”,又让它记住“公司文化、客服话术、语气规范”。这些信息全部拼在一个Context里,模型确实都能看到,但它分不清优先级,结果就是关键时刻该用的业务上下文被无关信息冲淡,或者该保持稳定的通用人格被业务异常数据带偏。
正交化的第三板斧,是把“记忆”也分层:
- 通用对话记忆:记录用户偏好、话题历史、交互风格,服务于通用推理。
- 业务状态记忆:记录当前业务流转到哪一步、已有字段值、待办动作,服务于业务深度。
- 领域知识库:静态的规则文档、FAQ、政策条款,需要时按需检索,而不是全程挂在上下文里。
我在项目里会用带命名空间的Memory对象,比如memory.set(“business.order_no”, “A12345”)和memory.set(“user.lang”, “zh-CN”)分开存。查询时,业务逻辑只能读业务命名空间,通用推理只能读通用命名空间。这样看起来有点麻烦,但能避免百分之八九十的上下文污染问题。
隔离做好了,你还可以更进一步:给记忆加读写审计。哪个模块写了什么,哪一段上下文被哪次工具调用修改过,到时候出了问题一看日志就知道是谁干的。
3. 从零搭一个“正交式”Agent:完整实操与代码走读
3.1 项目结构设计与依赖选择
下面我带你从零写一个最小可运行的正交化Agent,场景是“订单售后”。整个项目我们不依赖重量级Agent框架,只用Python标准库加一个可选的OpenAI SDK接口,目的是把正交化机制显式展示出来。如果你愿意,也可以把这个结构映射到LangChain、Dify这类框架上,思路是通用的。
先看目录结构:
ortho_agent/ ├── agent/ │ ├── core.py # Agent内核,只做感知/决策/行动 │ ├── memory.py # 分层记忆管理 │ └── tool.py # 工具与技能注册表 ├── skills/ │ ├── refund_skill.py # 售后退款判断技能 │ └── shipping_skill.py # 物流查询技能 ├── tools/ │ ├── order_tool.py # 查订单工具 │ └── notify_tool.py # 发通知工具 ├── runtime/ │ ├── openai_llm.py # 通用模型适配器 │ └── main.py # 启动入口 └── tests/ └── test_refund_flow.py这个结构其实就是把第2节说的“内核/技能/工具/记忆”落到了硬盘上。你注意看,agent目录下没有业务代码,skills和tools目录下没有模型调用。这就是正交的物理表达:谁依赖谁,一眼就能看出来。
依赖选择方面,我的建议是:如果项目以试错和学习为主,先用一个轻量的LLM封装层,自己定义好接口;等想清楚边界了再迁移到成熟框架。直接上重框架有一个风险——框架本身的抽象会“引导”你把业务逻辑写错位置。
3.2 核心代码实现:Agent内核、Skill接口、工具注册表
我们先把正交化的“边界”用代码定义出来。第一步是定义Skill和Tool的接口。
# agent/tool.py from abc import ABC, abstractmethod class BaseTool(ABC): """工具:执行一个原子操作,不包含业务决策逻辑""" name: str = "" description: str = "" @abstractmethod def run(self, **kwargs) -> dict: ... class BaseSkill(ABC): """技能:封装一个业务领域内的判断/流程逻辑""" name: str = "" description: str = "" input_schema: dict = {} @abstractmethod def run(self, agent: "Agent", user_input: str, context: dict) -> dict: ...为什么要有两个不同抽象?Tool是“没有脑子”的执行器,输入什么参数就返回什么结果;Skill是有“业务判断能力”的逻辑包,它需要读取上下文、调用若干Tool、按规则做决策。我把Skill放在“业务流程层”,Tool放在“基础设施层”。
接下来是Agent内核。它只做标准循环:感知输入、规划动作、调用Skill、返回输出。
# agent/core.py from typing import List, Dict, Any from .memory import AgentMemory from .tool import BaseTool, BaseSkill class Agent: def __init__(self, llm_backend, memory: AgentMemory, skills: List[BaseSkill], tools: List[BaseTool]): self.llm = llm_backend # 通用推理后端 self.memory = memory self.skills = {s.name: s for s in skills} self.tools = {t.name: t for t in tools} def perceive(self, message: str) -> Dict[str, Any]: """感知:解析用户输入,得到意图与关键实体""" return self.llm.perceive(message, self.memory.read("user")) def decide(self, perceive_result: Dict[str, Any]) -> str: """决策:根据感知结果,决定调用哪个技能""" prompt = f""" 你是一个通用任务调度器。用户输入已经被解析为如下意图和实体: {perceive_result} 在你的知识库中,你有以下可用技能: {list(self.skills.keys())} 请只输出你要使用的技能名称,不要输出任何解释。 如果所有技能都不匹配,输出 "unknown"。 """ return self.llm.complete(prompt).strip() def act(self, skill_name: str, user_input: str) -> str: """行动:执行技能,并把结果封装成自然语言回复""" if skill_name == "unknown" or skill_name not in self.skills: return "抱歉,我现在还处理不了这个问题。" skill = self.skills[skill_name] business_ctx = self.memory.read("business") result = skill.run(self, user_input, business_ctx) self.memory.write("business", result.get("state", {})) reply = self.llm.say(f"把下面的业务结果转成友好回复:{result}") return reply def handle_message(self, message: str) -> str: self.memory.write("user", {"last_message": message}) perceived = self.perceive(message) skill_name = self.decide(perceived) return self.act(skill_name, message)这里有个重要设计:Agent的decide是通用的,它只依据“感知结果+技能列表”做匹配,不关心技能内部逻辑。如果你新增一个“物流查询技能”,只需要在初始化时把它加进skills列表,其他代码不用改。这就是“业务深度外挂”的落地。
3.3 接入真实业务场景:以订单售后为例
现在我们来写一个具体的RefundSkill。它的职责是判断一笔订单能否退款,并把判断依据记录下来。它会调用两个Tool:一个查订单,一个发送通知。
# skills/refund_skill.py from agent.tool import BaseSkill class RefundSkill(BaseSkill): name = "refund" description = "处理售后退款申请,判断是否符合退款条件" def run(self, agent, user_input, business_ctx): # 1. 让通用模型从用户输入中抽取订单号 order_no = agent.llm.extract(user_input, "order_no") # 2. 调用通用工具查询订单 order_data = agent.tools["query_order"].run(order_no=order_no) # 3. 业务判断(这部分是领域知识,写死也不怕) reasons = [] if order_data["status"] != "delivered": reasons.append("订单未送达,不能直接退款") if order_data["refund_count"] >= 2: reasons.append("该订单退款次数过多,需要人工审核") if reasons: decision = "rejected" summary = ";".join(reasons) else: decision = "approved" summary = "符合退款条件" agent.tools["notify"].run(phone=order_data["phone"], msg="您的退款申请已通过") # 4. 返回结构化结果 return { "decision": decision, "summary": summary, "order_no": order_no, "state": {"last_order": order_no, "refund_status": decision} }这个Skill里面有一个问题需要专门说明:为什么订单查询本身没有并到Skill里?如果你把query_order的逻辑也塞进这个Skill,Skill就会变得“胖”,将来别的技能想查订单还得重新实现。把它拆成Tool之后,物流Skill、发票Skill都能复用同一个查询入口。
有人会担心这里用agent.llm.extract算不算“通用智能混进了业务层”。我的看法是:抽取实体是通用能力,不属于业务规则。只要Skill里不出现“如果是VIP客户且购买超过一个月才能退款”这种硬编码的业务策略,这个Skill就仍然是“业务逻辑外壳”,里面调用的还都是通用工具。
3.4 运行效果与参数调优
写完代码,我们跑一个例子。假设底层模型用的是本地部署的一个通用对话模型,温度设成0.2。控制台会是这样:
用户: 我上个月买的鞋有质量问题,想退掉。订单号是 A20240601。 感知结果: {'intent': 'refund_request', 'entities': {'order_no': 'A20240601'}} 决策结果: refund 技能执行: order_data={'status': 'delivered', 'refund_count': 1} 业务判断: approved, 符合退款条件 通知发送: 您的退款申请已通过 Agent回复: 您的退款申请已经通过了,我们会尽快为您处理。这里我建议新手把大模型的温度参数调低一点(0.1~0.3),尤其在decide这个环节。意图识别和技能路由属于“低随机性任务”,温度太高容易选错Skill。相反,在最后“生成友好回复”的环节,可以适当把温度调回0.7左右,让话术更自然。一个Agent里不同环节用不同参数,这本身就是正交化思想在参数配置上的体现。
如果你发现某个业务场景下模型经常“走神”,换用更专注的小模型往往比加更多提示词更有效。我试过用一个7B模型做意图识别,速度和成本都好看,而且因为任务单一,正确率比用大模型不低。
4. 从“能跑”到“稳定”:Agent开发中的常见问题与排查技巧实录
4.1 agent execution terminated due to error:先查工具,再查上下文
做Agent开发的人对这句话都熟,agent execution terminated due to error看起来像是一句笼统的系统报错,实际在正交化架构里,它背后通常就是三种原因之一。
第一是工具调用出了异常。比如Tool接收的order_no是None,说明实体抽取环节失败了。这时候先不要去看模型,而是看感知层有没有把订单号正确抽出来。我会在代码里给每个Tool调用包一层try...except,并在异常信息里带上入参,比如query_order received kwargs={...}。光是这一条就能省下一半的排查时间。
第二是上下文里的业务状态过期。最常见的是,用户先问了一单,又突然问“那这单呢?”,模型可能把上一单的订单号又复用了一遍。正交化架构里,业务记忆应该显式地隔离和清理。我建议在Skill结束时,把无关的state字段删掉,只保留本单业务流转必需的状态。很多Agent“跑飞了”不是模型傻,而是上一个业务的状态没清干净。
第三是推理链路太长导致模型失去焦点。如果你发现同一个错误反复出现,试着把Skill拆小。一个Skill里如果既要判断退款、又要计算补偿、又要写工单,那错误率必然高。拆成三个Skill让内核逐次调用,每个Skill只做一件事,稳定性会明显上升。
4.2 输出漂移与“幻觉”的抑制:正交化调试三板斧
业务Agent最怕的还不是报错,而是“一本正经地胡说八道”。比如用户问“我的快递到哪了”,物流Skill明明只查到一个未发货状态,模型的友好回复却是“您的包裹已达配送站”。这就是典型的输出漂移。
遇到幻觉,我有一套三板斧,全部围绕正交化原则展开。
第一板斧,检查业务结果是否结构化、是否与回复隔离。上面的例子中,Agent调用shipping_skill返回的是一个字典,里面status = pending,模型却编造了“已达配送站”,说明回复生成环节没有“守住底线”。正确做法是:给LLM一个明确的指令,比如“基于给定数据回复,不允许添加未提及的事实”,并且在提示词里把业务结果以JSON格式单独放一段,不让模型自由发散。
第二板斧,缩小技能内部的自由空间。如果某个业务场景对准确性要求极高,就不要让模型写自然语言结论,而是让它输出受控枚举值。比如RefundSkill的decision字段,可以预先定义为approved/rejected/manual_review三选一,而不是让模型自由填。业务规则越严,留给通用模型的自由度就要越小,这就是“业务深度”对“通用智能”的正交约束。
第三板斧,做好回归测试。每改一次业务规则,就把历史会话回放一遍,重点看之前能正确处理的案例有没有被改坏。我习惯在tests目录里塞一批“黄金用例”,用断言检查Agent返回里的业务结论字段。虽然不能完全自动化,但能拦下大部分回归问题。
4.3 框架与编排选型:harness、skill、agent到底怎么分工
市面上关于“Agent框架”“Agent编排”“Harness与Agent的区别”的讨论很多,很多人被术语绕晕。我按自己踩坑后的理解,用大白话给你捋一遍。
- Agent:一个能独立做决策的智能体,它“知道”自己可以调用哪些Skill和Tool,并能根据任务选择路径。
- Skill:Agent可复用的能力单元,封装“某个领域的一整套做事方法”。
- Harness:可以理解成“Agent的运行容器/执行环境”,负责编排多个Agent或者Agent与外围系统的交互,比如超时控制、权限校验、中间结果缓存,都是Harness的职责。
正交化设计里的一个分支标准是:如果一段逻辑关心“怎么调用模型”,那就属于Harness或内核;如果它关心“业务上该不该这么做”,那就属于Skill。把这两者混在一起写,后面维护会非常痛苦。
选型上,我的建议是:早期项目不要迷信“全家桶”框架。你自己用几十行代码实现一个极简内核之后,对“Agent、Skill、Tool”这三者的边界会理解得特别清晰。然后再去用别人现成的框架,你才能判断它的抽象是帮你还是坑你。反过来,如果项目已经成熟,直接用社区的编排框架可以少造轮子,但一定要确保业务技能是作为独立模块接进去的,而不是写进框架的配置里。
5. 正交平衡不是“五五开”:如何根据业务场景动态调整
5.1 什么时候通用多、业务少:探索型场景
很多人一听“正交平衡”,下意识认为就是通用智能占50%、业务深度占50%,这是误解。两个维度可以独立伸缩,它们的权重应该跟着场景走。
像“行业知识问答”“竞品调研分析”这类探索型场景,用户希望你放开思路、提供多元视角,业务规则只起“边界”作用。这种场景里,我在架构上会更偏重通用智能:Agent的决策链更长,允许它自由组合多个Skill,Skill内部也只放最粗粒度的限制(比如“不要回答超出公司经营范围的业务”)。如果这时候你塞入大量流程约束,用户会觉得这个Agent“死板”。
5.2 什么时候业务深、通用少:执行型场景
反过来,像“退款审批”“工单自动关闭”“风险交易拦截”这类执行型场景,系统价值在于确定性,而不在于发散。我的做法是弱化自由推理:Agent内核只做低层级意图识别,真正的动作完全由业务Skill主导,通用模型只负责理解少量歧义表达。
在这种场景里,我甚至会让技能返回时携带一个strict_mode字段,把回复模板固定住,模型只负责往里填用户可感知的话术。你说这还算Agent吗?算的,因为Task分解和Tool调用仍然是动态的;但它更像一个“戴着镣铐的Agent”,镣铐正是业务深度对通用智能的合法约束。
正交化的好处就在这里:你不用为了“通用Agent”的愿景牺牲业务效率,也不用以“业务脚本”为代价放弃智能交互。你可以按场景自由调节两个维度的强度,而核心架构不需要推倒重来。
5.3 用“正交解耦”反过来指导团队分工与代码组织
最后说一点团队层面的体会。
正交化不仅是一个技术架构,它还能直接影响团队协作方式。在我现在的项目里,负责通用能力的同学只维护agent内核和LLM适配层,不碰任何业务代码;负责业务的同学只维护skills和tools,不需要关心模型从哪个端点来、上下文窗口多大。两边通过接口文档对齐,效率比之前“大家一起改同一个prompt”高太多了。
如果你在一个人全栈做Agent,也建议在代码层面把目录分清楚,别让自己在同一个文件里既改模型调用又改退款规则。人的思维也是有上下文的,频繁切换会成倍增加出错概率。把组件拆开,就是给未来的自己减少“精神切换成本”。
文化上说,团队里要有一个共识:业务规则的修改不需要“模型升级”来承载,模型能力的升级也不应该打断业务节奏。当你的组员不再说“这个需求要等模型优化后才能接”的时候,属于你的正交化Agent就基本成了。
最后再分享一个小技巧。做Agent开发时,我给自己定的规矩是:每写一个Skill前,先写它的接口定义和一组测试用例,再动手写实现。这样能强迫你想清楚“这个技能的输入输出是什么、边界在哪”,而不是想当然地先把业务逻辑堆上去。坚持半年之后,你会发现“通用智能”和“业务深度”不再是对抗关系,它们成了一个系统里两个干净利落的坐标轴。希望这套思路,也能帮你在Agent开发路上少一些拉扯,多一些掌控感。