搞AI智能体最容易被忽略的不是模型选型而是全流程评估。我自己刚做智能体那会儿模型换了好几版Prompt也反复重写结果一到真实业务场景就翻车文档分类标签混乱邮件内容看起来没问题但完全不符合对方身份。后来我把整套流程拆开重做用LlamaAgents Builder做编排用Promptfoo做评测把文档分类数据和邮件撰写串在同一个智能体链路里底层模型用GPT-5.4才算是真正跑通了一条可复现的落地路径。这篇内容就是完整复盘整个开发过程包含关键代码、配置和踩坑记录适合正在做AI智能体应用尤其是文档处理、邮件生成这类想真正落地的朋友参考。1. 项目定位与整体架构拆解1.1 核心场景文档分类到邮件撰写的联动链路在真实业务里文档和邮件从来不是两件事。客户发来一封询价邮件我们不仅要知道这是“咨询”还要知道对方询的是哪款产品、大概数量、紧急程度然后才能回复一封有针对性邮件。同样客户发来一封投诉邮件系统要识别出这是高优先级问题、涉及哪个订单、希望得到什么补救方案再自动生成初审邮件的草稿交给人工确认。这个项目的核心就是把“文档分类”和“邮件撰写”连成一条串行的智能体流水线。智能体先读取原始文档抽取分类、优先级、关键实体再把这些结构化数据交给邮件智能体生成一封符合业务场景的邮件。整个过程要解决的不只是“识别得快不快”或“邮件像不像人写的”而是两头能否稳定衔接。这里有个容易被忽略的点分类不是终点。分类结果如果不被下游使用它的价值就是零。在这个项目里分类的标签体系要反过来从邮件写作需求设计。比如说邮件需要称呼对方姓名分类阶段就要提取联系人实体邮件需要回应客诉分类阶段就要识别投诉情绪强度和期望解决方案。所以动工第一件事不是写提示词而是梳理下游任务到底需要哪些字段再回头定义分类任务的输出格式。1.2 为什么选这套技术组合我拆过不少智能体项目选型时容易陷入两个极端要么全手动写一堆调度代码小流程也搞得像分布式系统要么全自动拖个界面但等到要评估效果时发现根本没法量化。这套组合算是平衡点。LlamaAgents Builder负责编排。它可以把分类Agent、邮件Agent定义成独立节点节点之间靠结构化数据传递。好处是后边想再加一个摘要Agent或者审批Agent不用重构主流程。配置走YAML可以直接纳入版本管理谁改了什么一目了然。Promptfoo负责评测。它的定位很像单元测试框架但测试对象是Prompt和模型输出。用YAML写一批固定用例每次改动后立刻能看到哪些用例通过、哪些失败这比肉眼测几遍可靠太多。尤其智能体链路上下协作上游分类输出稍微变一点下游邮件就可能跟着错没有自动回归测试根本扛不住。GPT-5.4负责核心推理能力。文档分类和邮件撰写都要求文本理解与生成能力同时GPT-5.4支持JSON结构化输出方便下游解析。我不选更重的微调方案是因为当前业务标签体系还在快速迭代用大模型做封闭式分类迭代成本远低于维护一个微调模型效果差距也没有想象中大。1.3 智能体的数据流转设计整个流程可以抽象成四个阶段文档预处理清洗文本、识别文件类型、去掉页眉页脚和乱码。分类推理输出JSON结构包含category、priority、entities、summary等字段。邮件上下文组装把分类字段映射成邮件模板需要的信息。邮件生成生成主题与正文支持人工审核。这条链路的重点是数据契约。两个Agent之间不直接传原始文档而是传固定结构的JSON。比如分类Agent输出的category字段只能从预定义集合里取值不能自由发挥否则邮件Agent根本无法判断应该走哪条话术分支。这个契约在设计初期就要定下来后期改字段成本很高我试过中途把category改成label结果一晚上都在修测试用例。2. 环境准备与工具链搭建2.1 环境依赖安装我默认你已经有了Python 3.10以上的环境。项目用到的核心包比较简单pip install llama-agents-builder promptfoo openai python-dotenvopenai用来调用GPT-5.4接口llama-agents-builder用来加载智能体配置并调度promptfoo通过它的CLI执行评估。密钥统一放在.env文件里代码里只需要load_dotenv()。需要说明的是llama-agents-builder不是那种很重的框架它更偏声明式Agent运行时。你定义YAML它负责调度。这对习惯写代码的人反而友好因为配置能进Git每个版本都可追溯。Promptfoo底层是Node工具但作为命令行用就行不需要在Python里写胶水代码。2.2 智能体编排配置在项目根目录建一份agents.yaml定义两个Agent以及它们之间的依赖关系。version: 1.0 agents: - name: doc_classifier role: classifier model: gpt-5.4 temperature: 0.1 input: document_content: string document_type: string output: category: string priority: string entities: object summary: string - name: email_writer role: writer model: gpt-5.4 temperature: 0.7 input: category: string priority: string entities: object summary: string output: subject: string body: string pipeline: - doc_classifier - email_writer两个Agent之间通过output和input的同名字段对齐。doc_classifier输出的category会作为email_writer的输入自动注入。这样写的好处是清晰坏处是字段命名如果有变动下游跟着断。所以字段命名一定要先定好后期尽量少动。这里category字段的候选值也要在后续分类提示词里写死。2.3 模型接入与推理参数调用GPT-5.4的时候我建议在请求参数里显式要求JSON输出。对大模型来说不给格式约束时输出里很容易夹带解释性文字比如“好的分类结果是”之类的开头。一旦要求JSON分类Agent的输出稳定性会明显提升。分类任务我设temperature0.1越低越好确保同一份文档多次分类结果基本一致。邮件撰写任务设temperature0.7保留一点创作空间。这个参数不是随便拍的而是任务性质决定的分类是判别任务随机性越低越好邮件是生成任务需要一些措辞变化否则批量场景容易显得特别模板化。3. 文档分类数据接入与智能体实现3.1 数据预处理与文本清洗原始文档形态很多有PDF、Word、扫描件。我的做法是先把它们统一转成纯文本再用脚本处理。PDF我用文档解析库抽文本Word转成docx后提取这些步骤不建议放进智能体里放在独立预处理阶段跑掉。真正要注意的是噪声控制。PDF页眉页脚、表格碎片、乱码字符都会干扰分类。一般我会过滤长度小于2的乱码行合并被打断的段落去掉页码信息。预处理的目标不是让文本变得好看而是让大模型拿到干净输入。实测里乱码对分类准确率影响很大有一次客户投诉文档因为乱码被分到“咨询”后续邮件话术直接带偏了还好有人工审核兜住。处理完的文档还需要保留基本信息比如文件类型、文档时间、内部的订单号、金额关键词位置。这些信息会传给分类Agent作为辅助字段帮助模型做判断。3.2 封闭式标签体系设计分类标签我强烈建议用封闭集合。开放标签虽然看着灵活但邮件Agent没法基于一个未知类别做话术选择。这个项目里我定义七个初始标签合同、发票、投诉、咨询、催款、通知、其他。每个标签还需要附带业务字段。比如投诉类需要priority字段取值范围是low、medium、high咨询类需要product字段和quantity字段。实际项目中我通常为每个类别设计独立的输出字段但主链路保持统一JSON结构。邮件Agent只需根据category判断走哪个分支再读取对应可选字段。标签命名也有讲究。比如“投诉”不要写成“客户投诉”“咨询”不要写成“业务咨询”。太长的名称会让模型纠结也会让输出字段值不稳定。我用两字或四字的固定词同时给Prompt里加一句说明如果没有完全匹配的类别输出other不要自行发明标签。3.3 分类Agent的代码实现有了上面配置实际调用代码很简洁。核心是加载配置、调用Agent、解析JSONimport json from llama_agents_builder import AgentRuntime from dotenv import load_dotenv load_dotenv() def classify_document(content: str, doc_type: str) - dict: runtime AgentRuntime.load(agents.yaml) classifier runtime.get_agent(doc_classifier) output classifier.run({ document_content: content[:8000], document_type: doc_type }) if isinstance(output, str): output json.loads(output) return output clean_text load_clean_text(some_document.pdf) result classify_document(clean_text, pdf) print(json.dumps(result, ensure_asciiFalse, indent2))输出结构类似下面这样{ category: complaint, priority: high, entities: { company: 某合作方, contact: A先生, order_no: PO-2024-0331 }, summary: 客户反馈交付延迟3天要求本周内确认补救方案。 }这里有个细节传给模型的内容不要用整个PDF文档尤其当文档很长时。我做了截断策略优先保留开头、结尾和包含订单号、金额的行。因为很多文档的核心诉求集中在首尾这种策略在分类任务里不会损失准确率反而能减少模型被中间无关信息干扰的可能。3.4 分类结果如何衔接邮件环节分类结果落地后不能直接丢给邮件Agent需要先做一层字段映射。比如categorycomplaint且priorityhigh邮件Agent要走“高优先级客诉”分支categoryinvoice则走“发票确认”分支。映射逻辑我放在代码里不写进Prompt。因为写进Prompt会让模型每次都要做业务判断占用上下文、增加不稳定。代码里写死的条件对于确定性规则更可靠def build_email_input(classification: dict) - dict: route { complaint: complaint_email, invoice: invoice_email, inquiry: inquiry_email }.get(classification[category], general_email) return { route: route, priority: classification[priority], entities: classification[entities], summary: classification[summary] }这一步就是所谓的“融合文档分类数据与邮件撰写”的关键位置。分类数据经过规则层之后才作为邮件Agent输入。这样设计的好处是哪怕模型分类对了业务规则也能防止它走错话术分支相当于加了一道确定性保险。4. 邮件撰写Agent让分类数据变成邮件内容4.1 邮件Prompt模板设计邮件Agent的Prompt不能只写“请写一封邮件”这是新手常见错误。我一般分成系统指令和用户指令两层。系统指令部分固定角色与约束你是某公司的商务运营助理。 根据用户提供的文档分类结果撰写邮件。 要求 1. 邮件必须以正确的称呼开始称呼来自entities.contact或entities.company 2. 正文必须复述文档摘要中的核心事实禁止编造订单号、金额、日期 3. 语气根据route字段变化投诉邮件要诚恳咨询邮件要热情且信息准确 4. 主题不超过20个字正文不超过150字 5. 只输出JSON格式为{subject: ..., body: ...}。用户指令部分传入结构化分类结果。这样邮件Agent不需要看原始文档只需要看压缩后的分类信息既省Token又不容易被无关文本带偏。4.2 为什么要在邮件中嵌入摘要直接让模型看原始文档再写邮件上下文会太长而且模型容易漏掉真正的核心诉求。利用分类阶段生成的summary等于提前帮模型做了一次信息压缩。实测里带summary的邮件比直接输入原文的邮件事实性错误率低了很多。但summary不能原封不动复制。我要求模型重写避免跟上游输出“撞词”。邮件正文至少要把summary里的主谓宾顺序调整一下比如把“客户反馈交付延迟3天”改成“本次交付延误了3天我们对此非常抱歉”。这样读起来更像自然沟通而不是机器搬运。另外邮件正文里不要出现“根据文档摘要”这类话。这是模型自己脑补出来的表述真实商务邮件里不会有人在正文里说“根据摘要”。这类口水话会让邮件一眼假需要在Prompt里明确禁止。4.3 邮件Agent的实现代码调用方式与分类Agent类似但需要确保输出是合法JSON。def generate_email(email_input: dict) - dict: runtime AgentRuntime.load(agents.yaml) writer runtime.get_agent(email_writer) output writer.run(email_input) if isinstance(output, str): output json.loads(output) required_keys {subject, body} missing required_keys - output.keys() if missing: raise ValueError(f邮件输出缺少字段: {missing}) return output email generate_email(build_email_input(result)) print(email[subject]) print(email[body])到这里一条完整链路已经跑通文档进入后自动输出一封可初审的邮件草稿。我通常会在邮件Agent之后再加一个人工审核节点审核通过才能发送。这个“人审”节点必须保留不能完全交给模型自动发信尤其面向外部客户的场景。4.4 邮件安全拦截图邮件自动生成最怕三种问题称呼错误、价格错误、情绪过激。我的做法是在输出之后加一段校验代码把常见价格、订单号从原始文档中抽出来与邮件正文交叉比对。如果邮件正文出现了原文没有的金额数字直接拦截不进入待发送队列。这个校验不能用模型做因为模型可能“合理推测”出错误价格。规则层用字符串匹配就能解决问题。比如文档里提取到订单号PO-2024-0331邮件正文里出现另一个订单号系统就要报警。这类校验虽然简单但在真实业务里能挡住大部分低级错误。5. 用Promptfoo搭建系统化评估5.1 为什么要系统化评估我一开始也是肉眼测效果改一版Prompt就手动试几个文档自我感觉良好。后来发现这种评估有个致命问题不可复现。很可能我改了一版Prompt后投诉类变好了但咨询类被改坏了肉眼根本发现不了。Promptfoo解决的就是这个问题。它把测试用例固化成YAML每次跑完给出通过、失败结果让我能准确知道改动影响了哪些类别。这套评估逻辑很像单元测试。模型和Prompt是被测代码测试用例代表业务预期。每次改动Prompt都要回归全部用例不改就不跑。这么做还有个附带好处新同事接手项目时能看到完整的用例库知道哪些点不能改坏。5.2 评估用例集构建用例不能太少。我准备了至少20条真实业务文档覆盖七类标签每类至少两条。投诉类里再细分高优先级和低优先级。每条用例至少包含三个断言分类是否正确、输出是否是合法JSON、邮件是否包含摘要里的关键实体。举一个用例样例用例编号输入文档内容期望标签期望优先级邮件关键断言001发票编号INV-2024-056金额5000元要求尽快付款invoicemedium正文需包含发票编号002订单PO-2024-0331延迟3天客户强烈不满complainthigh正文需道歉并给出补救时间003咨询产品A的报价计划采购100台inquirylow正文需提到产品A和报价引导这些用例内容要脱敏不能直接用真实客户名。我习惯用“某合作方”“A先生”代替。脱敏不影响断言逻辑但能避免评估结果或日志泄露客户隐私。5.3 Promptfoo配置与断言写法Promptfoo的核心是promptfooconfig.yaml。我把分类和邮件两个Prompt分别写在独立模板文件里然后统一配置prompts: - file://prompts/doc_classifier.txt - file://prompts/email_writer.txt providers: - openai:gpt-5.4 tests: - vars: document_content: 发票编号INV-2024-056金额5000元要求尽快付款。 document_type: pdf assert: - type: is-json - type: python code: assert output[category] invoice, output - type: python code: assert INV-2024-056 in output.get(summary, ), output - vars: route: complaint_email priority: high summary: 客户反馈交付延迟3天要求本周内确认补救方案。 entities: {company:某合作方,contact:A先生} assert: - type: python code: data __import__(json).loads(output)\nassert data[body][:20], data - type: contains value: 抱歉这里要注意测试输出会和原始模型输出格式强相关。最好先让分类Agent输出JSON再用Python断言解析。如果输出出现“好的分类结果是”这种前缀is-json断言会直接失败这其实是好事因为及时捕获了格式不稳定问题。5.4 评估结果与人工评分结合Promptfoo跑完会生成一份报告我能看到整体通过率和单条用例的差异。光看通过率不够我还会把失败用例归类是分类错、JSON格式错、还是邮件缺少关键字段。如果连续三次改动都有一类失败说明业务逻辑本身有问题而不是Prompt能解决的。我还会加一层人工评分。自动断言只查“有没有”不查“语气对不对”。所以我随机抽10条邮件人工打分满分为5分。低于3分的版本不进生产。这样做虽然麻烦但能防止GPT-5.4的自动评估把“看起来通顺但很假”的邮件评高分。6. 常见问题与排查技巧实录6.1 上下文太长导致费用飙升最开始我把整个文档都丢给GPT-5.4单次调用Token数经常超过2万跑一批评估用例费用很可观。后来我加了截断策略文档前2000字、后2000字再补上包含关键实体和金额的行。实测分类准确率没降费用降了七成。截断还有一个隐形好处模型不容易被中间大段不相关文本干扰。很多投诉信在中间会有一段背景叙述跟实际诉求关系不大留着反而让分类结果摇摆。6.2 分类结果不稳定如果同一份文档跑多次分类结果不一致第一个排查点是temperature。很多模型接口默认0.7对分类任务偏高我全部改成0.1。第二个排查点是标签命名太含糊的标签会让模型纠结。第三个排查点是少样本示例在Prompt末尾加两个分类示例效果很明显。我还会在分类Prompt里明确写“如果没有完全匹配的类别输出other不要自行发明标签。”否则模型容易输出标签的变体比如把“投诉”写成“客户问题反馈”下游规则匹配直接失效。这个坑我踩过好几次。6.3 邮件语气忽冷忽热投诉邮件和咨询邮件如果共用同一个Prompt但没有强约束模型很容易把投诉写得太正式而缺乏诚意或者把咨询写得过于推销。我的做法是给不同route各配一段风格示例。投诉示例是“先道歉、再确认问题、最后给补救时间”咨询示例是“先感谢、再给产品亮点、最后引导下一步沟通”。Promptfoo在这时特别有用。我给每个route各配两条测试用例专门断言邮件里是否出现道歉词或者是否包含产品名称。如果用例失败我能立刻定位是哪个route的Prompt出了问题。6.4 评估通过但人工感受不一致有几次Promptfoo报告全部通过但我读邮件觉得特别别扭。原因是自动断言只检查了关键词比如“抱歉”出现就算通过但邮件整体逻辑不通。所以我后来引入了混合评审自动断言筛掉硬伤人工抽查语感。如果自动全过但人工分数持续偏低我会调整Prompt中的风格示例而不是继续加断言。还有个小技巧把Promptfoo的输出导出每次评估后都与上一版报告对比。我不只看单次结果而是看趋势。如果一个版本所有指标都涨哪怕只是微弱涨幅我也会保留。如果有一项指标明显下降就坚决回滚不要心存侥幸。7. 一点个人体会这类智能体项目的坑不在某一个环节而在链路衔接。分类字段和邮件Prompt如果设计不同步后面改起来非常痛苦。我的体会是在写第一行代码之前先花一整天把数据契约定义清楚这个投入绝对值得。否则等到测试阶段再去补字段不只是改一个Agent的事而是要连带改下游提示词、评估用例、字段映射逻辑。另外评估不是上线前做一次就结束而是每次改Prompt都要跑一遍。Promptfoo就是那个能让你放心改Prompt的底牌。最后再分享一个小建议给邮件生成Agent永远留一个人工审核出口大模型可以帮你写好第一稿但最终发出去之前一定要有人眼确认。这个习惯能让你在推进自动化的时候少担很多风险。