1. 智能体开发的核心痛点:为什么你的Agent总在“猜”你的意图

做过智能体开发的人都有一个共同体会:Demo跑通只要一个下午,但要让它在真实业务里稳定干活,可能要磨上三个月。问题出在哪?绝大多数时候不是模型不够强,而是我们给智能体的“说明书”写得太烂了。

我见过太多团队的做法是这样的:拿到一个业务需求,直接打开Coze或者Dify,拖几个节点,写一段System Prompt,接上知识库,然后就开始测试。测试的时候发现效果还行,就上线了。上线之后用户随便换个说法,智能体就开始胡言乱语,要么答非所问,要么直接编造信息。这时候开发者的第一反应往往是“模型不行”,然后换更大的模型,换更贵的API,结果问题依旧。

这里面的核心矛盾在于: 大语言模型本质上是一个概率补全引擎,它不是在“理解”你的需求,而是在“猜测”你最可能想要什么 。你给它的指令越模糊,它的猜测空间就越大,输出就越不可控。所谓“别让智能体猜你要什么”,本质上是一个工程化问题——如何通过结构化的设计,把用户的模糊意图转化为智能体可以精确执行的确定性任务。

这个问题的难度在于,它横跨了三个层面: 意图理解层 (用户到底想要什么)、 任务编排层 (这个需求应该拆成几步来做)、 执行保障层 (每一步怎么保证不出错)。很多开发者只关注第一层,觉得Prompt写好了就万事大吉,结果在第二层和第三层反复踩坑。

我个人的经验是,一个能真正上生产的智能体,Prompt工程只占30%的工作量,剩下70%是工程化架构的设计。这70%包括:如何做意图路由、如何设计工具调用的边界条件、如何处理多轮对话中的上下文漂移、如何做输出格式的强约束、如何建立评测和回归机制。这些东西听起来很“重”,但恰恰是区分“玩具智能体”和“生产级智能体”的关键分水岭。

这篇文章适合两类人看:一类是正在做智能体开发但总觉得效果不稳定的工程师,另一类是对AI应用开发感兴趣、想系统了解工程化方法论的产品或技术负责人。我会从架构设计、核心细节、实操落地、问题排查四个维度,把智能体开发中那些“文档里不会写但实际一定会遇到”的东西讲清楚。

2. 智能体架构设计的底层逻辑:从“单点智能”到“系统工程”

2.1 为什么单Agent架构在复杂场景下必然翻车

刚开始接触智能体开发的时候,大多数人都是从单Agent开始的。一个System Prompt,几个工具函数,一个对话循环,看起来简洁优雅。这种架构在简单场景下确实够用,比如“查天气”“算数学题”“翻译一段话”,单Agent的表现甚至比多Agent更好,因为链路短、延迟低、出错概率小。

但一旦场景变复杂,单Agent的局限性就会暴露得非常明显。我拿一个实际的销售智能体项目举例。这个智能体的任务是:接待用户咨询、判断用户意向、推荐合适的产品、处理异议、引导下单。如果用一个Agent来做,System Prompt会变成什么样?它需要同时包含产品知识、销售话术、异议处理策略、下单流程、用户情绪识别规则……这段Prompt会膨胀到几千字,而且各个模块之间会互相干扰。

具体表现就是:当用户问一个产品参数问题时,Agent可能会突然切换到销售话术模式,开始推销;当用户表达犹豫时,Agent又可能退回到知识问答模式,机械地罗列产品优势。这种“人格分裂”的根源在于, 单个Agent的注意力是有限的,当Prompt中同时存在多个目标时,模型会在不同目标之间随机切换,而不是按照你期望的优先级来执行 。

更深层的问题是,单Agent架构下,你很难做精细化的错误归因。当输出不符合预期时,你无法判断是意图识别错了、知识检索错了、还是话术生成错了。所有环节耦合在一起,调试起来就像在一团乱麻里找线头。

2.2 多Agent编排的核心设计原则

多Agent架构的本质,是把一个复杂任务拆解成多个子任务,每个子任务由一个专门的Agent负责,通过编排层来控制它们之间的协作。这听起来像是微服务架构的思路,实际上确实如此。但多Agent编排有几个容易踩坑的地方,我逐个来说。

第一个原则是 职责单一 。每个Agent只做一件事,而且要把这件事做到极致。比如意图识别Agent就只负责判断用户当前这句话属于哪个意图类别,不要让它同时去检索知识库。工具调用Agent就只负责根据指令调用对应的API,不要让它去判断该不该调用。职责越单一,Prompt就越短,输出就越稳定,调试也越容易。

第二个原则是 显式路由 。Agent之间的跳转关系必须是显式定义的,不能靠模型自己“觉得”下一步该找谁。我见过一些项目用“让Agent自己决定调用哪个子Agent”的方式来做编排,结果就是路由混乱,经常出现循环调用或者跳过关键步骤的情况。正确的做法是,用一个路由层(可以是一个轻量级的分类模型,也可以是一组规则)来根据当前状态决定下一步走哪个分支。

第三个原则是 状态外置 。多Agent协作时,共享的上下文不能只放在对话历史里,必须有一个外部的状态管理器。这个状态管理器记录当前任务进行到哪一步、已经收集了哪些信息、下一步需要什么。这样做的好处是,每个Agent只需要读取自己关心的那部分状态,不需要把整个对话历史都塞进Prompt里,既节省Token又减少干扰。

2.3 编排层选型:Coze、Dify还是自建

现在市面上做智能体编排的平台不少,Coze和Dify是讨论度比较高的两个。我分别说一下它们适合什么场景。

Coze的优势在于上手快、插件生态丰富、和字节系的产品集成度高。如果你要做的是一个面向C端用户的轻量级智能体,比如客服机器人、内容推荐助手,Coze可以在很短时间内搭出一个可用的版本。但Coze的局限性也很明显:它的编排逻辑是可视化的,灵活性有限,复杂的条件分支和循环处理做起来很别扭;另外它的知识库检索策略是黑盒的,你很难做精细化的调优。

Dify的优势在于开源、可自部署、支持更复杂的编排逻辑。它的Workflow模式可以做出比较复杂的条件分支和并行处理,知识库也支持多种检索策略的配置。但Dify的学习曲线比Coze陡,而且自部署需要一定的运维能力。另外Dify的Agent模式(让模型自主决定调用哪些工具)在实际使用中稳定性一般,建议还是用Workflow模式来做确定性编排。

如果业务场景对稳定性、可控性要求极高,比如金融、医疗领域的智能体,我建议还是自建编排层。自建的核心工作其实不多:一个状态机来管理任务流转,一个路由模块来做意图分发,一套工具注册和调用机制,再加上日志和监控。这些用Python或者TypeScript写,一两周就能搭出雏形。自建的好处是,每一个环节你都能控制,出了问题能精确定位,优化的时候也有明确的抓手。

3. 意图理解与Prompt工程:让智能体“听懂人话”的关键技术

3.1 意图分类的工程化做法

意图理解是智能体开发的第一道关卡。用户说了一句话,智能体首先要判断这句话属于哪个意图类别,然后才能决定后续怎么处理。这个环节做不好,后面所有环节都是白搭。

很多开发者的做法是,把意图分类和后续处理放在同一个Prompt里,让模型一次性完成。比如:“用户说了一句话,请你判断他的意图,然后根据意图执行相应操作。”这种做法在意图类别少、边界清晰的时候勉强能用,但一旦意图类别超过5个,或者存在语义重叠的情况,准确率就会急剧下降。

更工程化的做法是,把意图分类独立成一个专门的步骤。具体来说,有两种实现路径。一种是 基于规则的分类 ,适合意图边界非常清晰、可以用关键词或正则表达式覆盖的场景。比如“查订单”意图,只要用户输入中包含“订单号”“物流”“发货”等关键词,就可以直接路由到订单查询流程。这种方式的优点是快、准、可控,缺点是覆盖不了多样化的表达。

另一种是 基于模型的分类 。可以用一个小模型(比如BERT级别的分类模型)来做意图分类,也可以用大模型通过Few-shot Prompt来做。用大模型做分类的关键是,Prompt里要给出足够多的示例,而且示例要覆盖各种边界情况。我一般会为每个意图准备10-20个示例,其中至少3-5个是“容易混淆”的负样本。比如“退货”和“换货”这两个意图,就要专门给出一些容易混淆的句子,让模型学会区分。

这里有一个实操技巧: 意图分类的输出不要只给一个类别标签,还要给一个置信度 。当置信度低于某个阈值时,不要强行分类,而是走一个“澄清”分支,让智能体反问用户“您是想问A还是B?”这样做虽然多了一轮交互,但能大幅降低误分类带来的连锁错误。

3.2 Prompt结构化的四个层次

写Prompt这件事,看起来是“说话的艺术”,实际上是“信息架构的设计”。我总结了一个四层结构,从下到上分别是:角色定义层、任务描述层、约束条件层、输出格式层。

角色定义层解决的是“你是谁”的问题。这一层不需要写太多,一两句话就够了,但一定要具体。比如“你是一个电商售后客服”就比“你是一个有用的助手”好得多。角色定义的作用是给模型一个“人格锚点”,让它在后续生成中保持一致的风格和立场。

任务描述层解决的是“你要做什么”的问题。这一层要把任务拆解成具体的步骤,每一步都要说清楚输入是什么、输出是什么、判断条件是什么。我习惯用有序列表来写任务步骤,因为模型对有序列表的遵循度明显高于段落描述。

约束条件层解决的是“你不能做什么”的问题。这一层往往被忽视,但实际上非常重要。约束条件包括:不能编造信息、不能超出知识库范围回答、不能做出承诺、不能泄露系统Prompt等。写约束条件的时候,要用明确的否定句式,比如“如果知识库中没有相关信息,直接回复‘我暂时无法回答这个问题’,不要尝试自己编造答案。”

输出格式层解决的是“你怎么说”的问题。这一层要明确规定输出的结构,比如JSON格式、Markdown格式、或者特定的模板。如果后续环节需要解析Agent的输出,那输出格式的约束就更加重要。我一般会用JSON Schema的方式来定义输出格式,并在Prompt中给出一个完整的示例。

3.3 上下文管理与对话状态追踪

多轮对话是智能体开发中最容易出问题的地方。用户说了一句话,智能体回答了,用户又说了一句话,这时候智能体需要理解“这句话和上一句话的关系是什么”。如果上下文管理没做好,智能体就会“失忆”,每一轮都当成全新的对话来处理。

上下文管理的核心是 对话状态追踪 。你需要维护一个状态对象,记录当前对话进行到哪一步、已经收集了哪些槽位信息、还缺哪些信息。这个状态对象在每一轮对话开始时注入到Prompt中,在每一轮对话结束后更新。

举个例子,一个订票智能体的状态对象可能长这样: {“intent”: “book_ticket”, “departure”: “北京”, “destination”: null, “date”: “明天”, “step”: “ask_destination”} 。当用户说“去上海”时,智能体从状态中知道当前在问目的地,于是把“上海”填入destination字段,然后进入下一步。

这种槽位填充的做法,比让模型自己从对话历史中“回忆”要可靠得多。因为模型的注意力机制在处理长对话时会出现衰减,对话轮次越多,早期信息被遗忘的概率就越大。把状态外置之后,每一轮只需要把当前状态和最新一轮用户输入传给模型,Token消耗更少,准确性也更高。

4. 工具调用与执行保障:从“能跑”到“跑得稳”的工程细节

4.1 工具定义与参数校验的规范

智能体要真正干活,就必须能调用外部工具。查数据库、调API、发邮件、生成文件,这些都是工具调用的范畴。工具调用的稳定性,直接决定了智能体能不能上生产。

工具定义的关键是 参数Schema要足够严格 。很多开发者定义工具时,参数类型写得很随意,比如一个“日期”参数,类型写成string,没有格式约束。结果模型有时候传“2024-01-01”,有时候传“明天”,有时候传“下周一”,后端解析的时候直接崩溃。

正确的做法是,每个参数都要有明确的类型、格式、取值范围和是否必填的标记。日期参数就用 format: “date” 来约束,枚举参数就用 enum 来限定可选值,数值参数就用 minimum 和 maximum 来限定范围。这些约束不仅写在工具定义里,也要在Prompt中明确告诉模型。

另外, 工具描述要写清楚“什么时候用”和“什么时候不用” 。比如一个“查询订单”工具,描述里要写:“当用户提供了订单号,并且明确表达了查询订单状态的意图时,调用此工具。如果用户只是问‘我的快递到哪了’但没有提供订单号,不要调用此工具,而是先询问订单号。”这种正反两面的描述,能大幅减少误调用。

4.2 工具调用失败的处理策略

工具调用失败是常态,不是异常。网络超时、API限流、参数错误、权限不足,这些情况在实际运行中都会遇到。如果智能体没有处理失败的能力,一旦工具调用失败,整个对话就会卡死或者给出莫名其妙的回复。

我一般会设计三层失败处理机制。第一层是 重试 ,对于网络超时这类临时性故障,自动重试2-3次,每次间隔递增。第二层是 降级 ,如果重试仍然失败,切换到备用方案。比如查询数据库失败,就改为从缓存中读取;调用外部API失败,就返回一个预设的兜底回复。第三层是 转人工 ,如果降级也失败,就把对话转接给人工客服,同时把失败原因和上下文一并传递过去。

这里有一个细节: 失败处理逻辑不要写在Prompt里让模型自己判断 。模型对“失败”的理解很不稳定,有时候工具返回了一个错误码,模型却当成正常结果继续处理。正确的做法是,在代码层面拦截工具调用的返回结果,根据错误码走对应的处理分支,只把处理后的结果告诉模型。

4.3 输出格式的强约束与解析

如果智能体的输出需要被下游系统解析,那输出格式的约束就是生死线。我见过太多项目,因为模型偶尔多输出了一句话、少了一个括号、把JSON的key写错了,导致整个流程崩溃。

强约束输出格式的方法有几个。最直接的是 用JSON Schema做约束 ,在调用模型API时指定 response_format 为JSON,并传入Schema。这样模型会强制按照Schema输出,格式错误的概率大幅降低。但要注意,不是所有模型API都支持这个功能,而且即使支持,也不能保证100%不出错。

更稳妥的做法是 在Prompt中给出完整的输出示例,并在代码层面做校验和修复 。具体来说,Prompt里要明确写“你的输出必须是一个合法的JSON对象,不要包含任何其他文字”,然后给出一个完整的示例。代码层面,拿到模型输出后,先用正则提取JSON部分,然后用JSON解析器解析,如果解析失败,尝试自动修复(比如补全缺失的括号、修正常见的格式错误),如果修复也失败,就触发重试或降级。

还有一个经验: 输出字段的命名要尽量简单、无歧义 。不要用 user_intention_category 这种又长又容易拼错的字段名,用 intent 就够了。字段越少、越简单,模型出错的概率就越低。

5. 实操落地:从零搭建一个可用的智能体项目

5.1 环境准备与技术栈选型

假设我们要搭建一个电商售后智能体,功能包括:查询订单状态、申请退货、查询退款进度、转人工客服。我来说一下完整的技术栈选型和搭建过程。

模型方面,如果预算充足且对效果要求高,可以用GPT-4或者Claude 3.5。如果考虑成本和数据隐私,可以用国内的通义千问、文心一言,或者自部署开源模型。自部署的话,7B级别的模型(如Qwen2-7B)在意图分类和简单对话上已经够用,但复杂推理还是得用更大的模型。我个人的建议是: 意图分类和槽位提取用小模型,复杂对话生成用大模型 ,这样成本和效果的平衡最好。

编排框架方面,如果团队没有历史包袱,我建议用Dify的Workflow模式起步。它的可视化编排能快速验证流程,而且支持自定义代码节点,灵活性也够。如果业务逻辑特别复杂,或者需要和现有系统深度集成,那就自建编排层,用FastAPI或者Express写后端,用Redis做状态管理。

知识库方面,如果售后政策、产品信息这些内容比较多,建议用向量数据库做检索。Milvus、Qdrant、Chroma都是不错的选择。检索策略上,我一般用“向量检索+关键词检索”的混合模式,向量检索负责语义匹配,关键词检索负责精确匹配,两者结合能覆盖更多场景。

5.2 核心流程的代码实现

下面我用Python伪代码来展示核心流程的实现。首先是状态定义:

from dataclasses import dataclass, field
from typing import Optional, List

@dataclass
class ConversationState:
    intent: Optional[str] = None
    order_id: Optional[str] = None
    refund_reason: Optional[str] = None
    step: str = "intent_recognition"
    history: List[dict] = field(default_factory=list)
    retry_count: int = 0

然后是意图识别模块:

def recognize_intent(user_input: str, state: ConversationState) -> str:
    prompt = f"""
    你是一个意图分类器。请判断用户输入属于以下哪个意图:
    - query_order: 查询订单状态
    - apply_refund: 申请退货
    - query_refund: 查询退款进度
    - human_service: 转人工客服
    - other: 其他

    用户输入:{user_input}
    只输出意图标签,不要输出其他内容。
    """
    result = call_llm(prompt, model="small_model")
    intent = result.strip()
    if intent not in ["query_order", "apply_refund", "query_refund", "human_service", "other"]:
        intent = "other"
    return intent

工具调用模块:

def call_tool(tool_name: str, params: dict) -> dict:
    max_retries = 3
    for i in range(max_retries):
        try:
            if tool_name == "query_order":
                return order_service.query(params["order_id"])
            elif tool_name == "apply_refund":
                return refund_service.apply(params["order_id"], params["reason"])
            elif tool_name == "query_refund":
                return refund_service.query(params["order_id"])
        except TimeoutError:
            if i == max_retries - 1:
                return {"error": "timeout", "message": "服务暂时不可用"}
            time.sleep(2 ** i)
        except Exception as e:
            return {"error": "unknown", "message": str(e)}

主循环:

def handle_conversation(user_input: str, state: ConversationState) -> str:
    if state.step == "intent_recognition":
        state.intent = recognize_intent(user_input, state)
        state.step = "slot_filling"
        return handle_slot_filling(state)
    elif state.step == "slot_filling":
        return handle_slot_filling(state)
    elif state.step == "tool_execution":
        return handle_tool_execution(state)

这个框架看起来简单,但每个环节都有很多细节可以打磨。比如意图识别环节,如果置信度低,可以加一个澄清步骤;槽位填充环节,如果用户提供了多个信息,要能一次性提取;工具执行环节,要根据返回结果决定是继续追问还是给出最终回复。

5.3 评测与回归:怎么知道你的智能体“变好了”

智能体开发最怕的就是“感觉变好了”。你改了一版Prompt,测试了几个case,觉得效果不错,就上线了。结果上线后发现,之前能正常处理的case现在反而出错了。这就是没有做评测和回归的后果。

评测的第一步是 建立测试集 。测试集要覆盖所有意图类别,每个类别至少20-30个case,而且要包含各种边界情况:表达模糊的、包含多个意图的、有错别字的、中英文混杂的。测试集建好之后,每次修改Prompt或调整流程,都要跑一遍全量测试,对比准确率的变化。

评测的第二步是 定义指标 。意图分类的指标就是准确率和召回率,工具调用的指标是成功率和参数正确率,对话生成的指标可以用人工评分或者用另一个模型来打分。指标要量化,不能靠感觉。

评测的第三步是 持续监控 。上线之后,要记录每一轮对话的完整链路:用户输入、意图识别结果、工具调用参数、工具返回结果、最终输出。这些日志不仅是排查问题的依据,也是优化Prompt的素材。我习惯每周从日志中抽样100条对话,人工检查有没有问题,然后把有问题的case补充到测试集中。

6. 常见问题与排查技巧实录

6.1 意图识别不准的排查思路

意图识别不准是最常见的问题。排查的时候,我一般按照以下顺序来定位。

先看 测试集本身有没有问题 。有时候不是模型不准,而是测试集的标注就有歧义。比如“我要退货”这句话,标注成“apply_refund”没问题,但“这个订单我不想要了”到底算“apply_refund”还是“query_order”,不同人可能有不同理解。遇到这种情况,要先统一标注标准。

再看 Prompt中的意图定义是否清晰 。如果两个意图的边界模糊,模型就很难区分。比如“查询退款进度”和“查询订单状态”,如果订单状态里也包含退款状态,那模型就会混淆。解决办法是在Prompt中明确写出每个意图的边界,并给出容易混淆的负样本。

最后看 模型能力是否足够 。如果意图类别很多(超过10个),或者语义非常接近,小模型可能确实搞不定。这时候要么换大模型,要么把意图分类拆成两级:先分大类,再分小类。

6.2 工具调用参数错误的解决路径

工具调用参数错误通常有三种原因:模型没理解参数格式、模型没拿到必要的信息、模型产生了幻觉。

如果是格式问题,就在工具定义和Prompt中把格式约束写得更明确。比如日期参数,不要只说“日期”,要说“格式为YYYY-MM-DD的日期字符串”。

如果是信息缺失,就要在调用工具之前加一个校验步骤。比如查询订单需要订单号,如果状态里没有订单号,就不要调用工具,而是先问用户要订单号。

如果是幻觉,比如用户没提供订单号,模型自己编了一个,那就要在Prompt中强调“不要编造任何参数值,如果缺少必要参数,向用户询问”。同时在代码层面做校验,如果参数值不在合理范围内,直接拦截。

6.3 多轮对话中上下文丢失的修复方法

上下文丢失的典型表现是:用户在前一轮说了“我要退货”,智能体问了订单号,用户提供了订单号,智能体却问“请问您要办理什么业务?”这就是状态没有正确传递。

排查的时候,先检查状态对象有没有在每一轮正确更新。很多时候是代码逻辑的问题,比如状态更新写在了错误的回调里,或者状态在传递过程中被覆盖了。

如果状态更新没问题,那就是Prompt注入的问题。检查每一轮传给模型的Prompt中,有没有包含当前的状态信息。我一般会在Prompt的开头加一段:“当前对话状态:{state_json}”,让模型始终能看到最新的状态。

还有一个容易忽视的点是 对话历史的截断 。如果对话轮次很多,为了节省Token,可能会截断历史。截断的时候要注意,不要把包含关键信息的那一轮截掉了。我的做法是,只保留最近5轮对话,但状态对象始终完整传递。

6.4 常见问题速查表

问题现象 可能原因 排查方法 解决方案
意图识别错误 Prompt定义模糊 检查意图边界是否清晰 补充负样本,明确边界
工具调用参数错误 参数格式未约束 检查工具Schema 增加格式约束和校验
多轮对话失忆 状态未正确传递 检查状态更新逻辑 状态外置,每轮注入
输出格式错误 约束不够强 检查Prompt和解析逻辑 用JSON Schema约束
响应延迟高 模型调用链路过长 检查调用链路 并行调用,缓存结果
重复调用工具 路由逻辑有误 检查状态机流转 显式路由,加去重逻辑

7. 智能体工程化的经验总结与进阶方向

做智能体开发这一年多,我最大的体会是: 技术选型不是最重要的,工程化思维才是 。同样的模型、同样的框架,有的人做出来的智能体稳定可靠,有的人做出来的三天两头出问题,差距就在工程化的细节里。

几个我觉得特别重要的经验。第一, 永远不要相信模型的“自觉性” 。模型不会“理解”你的意图,它只是在做概率补全。所以每一个环节都要有明确的约束和校验,不能把关键逻辑交给模型自己判断。第二, 状态管理是智能体的骨架 。没有状态管理的智能体就像没有记忆的人,每一轮对话都是全新的开始。把状态外置、显式管理,是稳定性的基石。第三, 评测和监控不是可选项 。没有评测,你就不知道改动是变好了还是变差了;没有监控,你就不知道线上到底发生了什么。

进阶的方向,我觉得有几个值得关注。一个是 多智能体协作 ,让多个专业Agent通过消息传递来协同完成复杂任务,这在复杂业务场景下会越来越重要。另一个是 智能体的自我优化 ,通过收集线上反馈来自动调整Prompt和流程,减少人工调优的成本。还有一个是 智能体与现有系统的深度融合 ,不是把智能体当成一个独立的聊天窗口,而是把它嵌入到现有的业务流程中,成为系统的“智能层”。

最后分享一个我踩过的坑:不要试图用一个智能体解决所有问题。我刚开始做的时候,总想着做一个“万能助手”,什么都能干。结果就是什么都干不好。后来把场景收窄,一个智能体只解决一个具体问题,效果反而好了很多。智能体开发这件事, 做减法比做加法难,但做减法才是正确的方向 。

更多推荐