AI Agent智能体项目的交付速度往往和它变烂的速度一样快。很多团队用两三周就能做出一个会调用工具、能处理用户问题的 Agent但上线之后不到一个季度代码库就进入“改一行 prompt 都要提心吊胆”的状态业务规则散落在系统提示词里工具注册函数分布在七八个文件里对话历史只增不减模型偶尔返回一段异常 JSON 只能靠硬重试。开发圈把这种状态叫“屎山”。相比传统后端Agent 屎山的清理难度更高因为它同时涉及代码、模型行为、提示词、工具协议和上下文管理。也正因为如此针对 Agent 技术债的清理、重构和治理已经开始成为一种实际付费的技术服务。下面从工程角度拆开讲清楚Agent 屎山是怎么形成的怎样体检怎样分层重构怎样验证以及为什么这项工作具备专业服务价值。1. Agent 项目的屎山和传统后端屎山不是一回事1.1 Agent 比普通后端多了一层“模型行为”传统后端的技术债通常表现为模块划分混乱、函数过长、接口语义不清晰、依赖没有收敛。这些问题虽然让人头疼但至少是确定的代码在跑什么读代码就能看出来错误也能用断点和日志定位。Agent 系统不一样。一个典型的 Agent 至少包含三层模型层大语言模型负责理解用户意图、决定调用哪个工具、组织最终回答。工具层Agent 通过工具函数读写外部数据完成订单查询、库存判断、文档检索等真实操作。编排层负责维护对话循环把模型输出、工具调用结果和终止条件串起来。问题就出在“第三层”上。Agent 的最终行为不完全是代码写死的而是模型根据 prompt 和工具描述即时决定的。同一段代码换一个模型、改一个词的 prompt行为就可能漂移。这意味着清理 Agent 屎山时不能只保证“代码能编译”还必须保证“模型行为等价”。这是它和普通后端重构最本质的区别。1.2 屎山形成的三条典型路径结合实际项目Agent 代码库快速腐化通常有三条路径。第一条是 prompt 膨胀。初版 prompt 只有几句话随着业务规则增加开发者在 system prompt 里不断追加判断分支比如“当用户提到退货时先调用退货工具”“当工具返回超时时先道歉再重试”“如果是 VIP 用户要优先处理”。最后 prompt 变成几百行说明书模型服从度下降token 成本上升而且没人敢删任何一句因为不知道哪句话会影响线上行为。第二条是工具层和业务逻辑耦合。工具函数本来应该只做输入校验、调用外部服务、返回结构化结果。但很多项目把鉴权、缓存、重试、日志甚至页面跳转都写在工具函数里工具之间还互相调用形成一张没有文档的依赖网。模型调用工具时参数稍有偏差整个逻辑就崩。第三条是上下文和状态管理缺失。对话历史无限追加用户闲聊、工具返回的长文本、上一次失败的报错全部堆在 messages 里。短期内还能跑越到后期越慢token 消耗越来越大模型还会被早期错误信息带偏。清理时要补记忆策略而不是简单把 messages 一删了事。1.3 清理前先分清是架构问题还是代码问题动手之前要做一次判断这个 Agent 的问题是架构设计导致的还是具体代码写得差导致的架构问题指分层缺失、工具协议不统一、没有状态管理、没有可观测性。这些问题靠改局部代码解决不了必须调整工程结构。代码问题指单个函数写得丑、参数命名混乱、缺少异常处理这些可以通过局部重构解决。如果架构问题被当成代码问题处理结果是每天修修补补屎山还在。如果代码问题被当成架构问题处理结果是无限期重写业务长期停摆。合理的做法是先把两类问题分开架构问题用结构调整解决代码问题在结构里逐步收敛。2. 先给 Agent 项目做一次技术债体检2.1 五个最容易烂的位置清理屎山前先按下面五个位置做一次体检。不要凭感觉评估要落到文件和代码上。体检位置典型症状潜在风险Prompt 维护system prompt 超过 2000 token包含大量场景分支模型服从度下降token 成本上升行为漂移工具注册工具函数散落多处参数没有 schema靠模型猜无法复用模型调用格式混乱调试困难编排循环工具调用、错误重试、终止条件写在同一段函数里难以测试难以定位死循环和超时记忆与上下文历史消息无限追加没有摘要和裁剪响应变慢成本失控模型被旧错误带偏可观测性只有 print没有结构化日志没有 trace_id线上问题无法定位重构无依据体检时有几个检查动作统计 system prompt 的 token 数列出所有工具函数所在文件搜索循环里有没有步数上限看 messages 长度是否随会话无限增长检查日志里有没有工具名、参数、耗时和执行结果。2.2 一份“能跑但很臭”的最小示例下面这段代码模拟了很多 Agent 项目的真实状态能跑但所有问题都缠在一起。先看代码再逐条找问题。# order_agent_v1.py # 这段代码能跑通演示但属于典型的 Agent 屎山逻辑全在提示词和主函数里 import json from langchain_openai import ChatOpenAI llm ChatOpenAI(modelgpt-4o-mini) USER_PROMPT 你是订单客服。用户问订单状态时先调用 query_order用户要退货时调用 return_order。 TOOL_DESC 可用工具query_order(order_id) 查询订单return_order(order_id, reason) 提交退货 def run_agent(user_input: str) - str: messages [ {role: system, content: USER_PROMPT}, {role: user, content: user_input}, {role: user, content: TOOL_DESC}, ] # 模型没有真正绑定工具只能靠提示词“看懂”工具格式 resp llm.invoke(messages) content resp.content.strip() # 如果模型直接返回一段 JSON这段解析就非常脆弱 if content.startswith({): action json.loads(content) if action[name] query_order: result {order_id: action[args][order_id], status: shipped} return f订单状态{result[status]} return content这段代码有几个典型问题工具列表写在提示词里而不是注册给模型框架模型是否返回 JSON、返回什么字段都不可控。工具函数没有真正实现只是把静态数据塞进返回结果无法支撑真实业务。没有异常处理。模型返回的 JSON 字段缺一个json.loads直接抛异常。没有步数上限。如果模型连续返回 JSON代码循环只能靠递归或外部中断停止。没有日志。线上出问题根本不知道是模型调用失败、工具失败还是解析失败。2.3 把体检结果转成可执行的重构清单体检之后不要直接动手改代码先输出一张重构清单。每条清单包含三列现状问题、目标状态、验收方式。现状问题目标状态验收方式工具列表写在系统提示词里工具通过框架注册模型走原生工具调用协议打印 tool_calls确认不靠解析 JSON工具函数返回静态数据工具函数有 schema 校验和真实服务调用单测覆盖正常、异常、超时输入Agent 循环没有步数限制编排层有 MAX_STEPS 和终止条件构造循环场景确认到步后退出消息历史无限增长上下文有摘要策略检查 messages 长度是否被裁剪日志只有 print结构化日志包含 step、tool、耗时从日志能还原一次完整执行链路这份清单就是重构的边界。没有写进清单的问题不在本次清理范围内避免清理过程中不断扩项最后从重构变成重写。3. 清理屎山的三刀分层、拆分、可观测3.1 第一刀把 Agent 拆成提示词、工具、编排三层清理 Agent 屎山核心不是把代码写漂亮而是把三个经常缠在一起的部分拆开提示词层只放系统角色、行为约束和少部分必要的业务规则。工具层每个工具负责一个独立动作有明确的输入输出 schema。编排层负责维护对话循环、调用工具、处理异常、判断终止。提示词 YAML 独立出来之后可以单独评审、单独做版本对比不用为了改一句话去翻代码。# config/prompts.yaml system: | 你是一个订单客服助手。 - 用户咨询订单状态时使用 query_order 工具。 - 用户申请退货时使用 return_order 工具。 - 工具调用失败时如实说明不要虚构订单状态。 - 回答使用中文尽量简洁。注意业务规则不要全部塞进提示词。能落在代码里的规则比如步数限制、参数校验、重试策略全部落在代码里。提示词只保留模型必须理解的内容比如角色、工具选择依据、回答风格。3.2 第二刀让工具定义可配置、可校验工具层重构的目标是每个工具有独立文件和独立 schema有输入校验调用外部服务时自带异常处理。# tools/order_tools.py from pydantic import BaseModel class QueryOrderInput(BaseModel): order_id: str class ReturnOrderInput(BaseModel): order_id: str reason: str def query_order(args: dict) - dict: # 实际项目替换成真正的订单服务调用 order_id args[order_id] return {order_id: order_id, status: shipped} def return_order(args: dict) - dict: return {ok: True, order_id: args[order_id]} TOOLS { query_order: { description: 根据订单号查询订单状态, schema: QueryOrderInput.model_json_schema(), handler: query_order, }, return_order: { description: 提交退货申请, schema: ReturnOrderInput.model_json_schema(), handler: return_order, }, }这样写好的一套 TOOLS 结构既能传给 LangChain 等框架做工具注册也能传给自定义编排层让模型按 schema 调用。工具函数内部只做一件事解析参数、调用业务服务、返回结果。鉴权、缓存、日志这些横切关注点在工具外部统一处理而不是写进每个工具。3.3 第三刀补上步数限制、日志和 trace编排层是整个 Agent 的骨架。这里最容易写成一整段 if else也最需要补结构。下面这段是重构后的最小编排循环# core/agent_loop.py import logging logger logging.getLogger(__name__) MAX_STEPS 5 def find_tool(tools: dict, name: str): tool tools.get(name) if tool is None: raise ValueError(funknown tool: {name}) return tool def run_agent(llm, tools: dict, system_prompt: str, user_input: str) - str: messages [ {role: system, content: system_prompt}, {role: user, content: user_input}, ] for step in range(1, MAX_STEPS 1): logger.info(step%s messages%s, step, len(messages)) response llm.invoke(messages) tool_calls getattr(response, tool_calls, []) or [] if not tool_calls: logger.info(agent finished at step%s, step) return str(response.content) messages.append(response) for call in tool_calls: logger.info(call tool%s args%s, call[name], call[args]) tool find_tool(tools, call[name]) try: tool_result tool[handler](call[args]) content str(tool_result) except Exception as exc: # 单个工具失败不应该中断整个 Agent把错误返回给模型继续决策 logger.exception(tool failed name%s, call[name]) content ferror: {exc} messages.append({ role: tool, tool_call_id: call[id], content: content, }) raise RuntimeError(fagent exceeded max steps: {MAX_STEPS})这段编排逻辑解决了几件事步数上限避免模型连续调用工具形成死循环。工具异常隔离单个工具抛异常时把错误信息作为 tool 消息返回给模型让模型决定如何处理而不是直接中断整个 Agent。结构化日志每步记录 messages 数量、工具名、参数后续排查有据可查。工具名不存在时抛出明确错误而不是静默忽略。3.4 重构后的最小工程结构清理完的仓库结构应该一眼能看出分层order_agent/ ├── config/ │ ├── prompts.yaml │ └── settings.py ├── tools/ │ ├── __init__.py │ └── order_tools.py ├── core/ │ ├── __init__.py │ └── agent_loop.py ├── tests/ │ ├── __init__.py │ └── test_agent_loop.py └── main.py入口 main.py 只负责组装依赖读配置、初始化模型、加载工具、调用编排层。# main.py import logging import yaml from langchain_openai import ChatOpenAI from tools.order_tools import TOOLS from core.agent_loop import run_agent logging.basicConfig(levellogging.INFO) if __name__ __main__: with open(config/prompts.yaml, r, encodingutf-8) as f: prompt_config yaml.safe_load(f) llm ChatOpenAI(modelgpt-4o-mini) answer run_agent( llmllm, toolsTOOLS, system_promptprompt_config[system], user_input查一下订单 2025001 的状态, ) print(answer)这里要特别说明不同版本的语言模型框架response.tool_calls的结构可能不同。有的版本用additional_kwargs[tool_calls]有的版本直接挂在tool_calls属性上。落地时先打印一次响应结构确认字段再写解析逻辑不要照抄。4. 重构之后怎么证明 Agent 没有变蠢4.1 先用 Fake LLM 写回归测试清理屎山最大的风险不是代码结构变差而是重构后模型行为变了。要证明 Agent“没有变蠢”最稳妥的方法是在编排层测试中替换真实模型用一个固定返回的 Fake LLM把工具调用链路完整跑一遍。# tests/test_agent_loop.py from core.agent_loop import run_agent class FakeResp: def __init__(self, content, tool_callsNone): self.content content self.tool_calls tool_calls or [] class FakeLLM: def __init__(self, schedule): self.schedule schedule self.calls 0 def invoke(self, messages): resp self.schedule[self.calls] self.calls 1 return resp def test_query_order_success(): fake_llm FakeLLM([ FakeResp(contentNone, tool_calls[ {id: call_1, name: query_order, args: {order_id: 2025001}} ]), FakeResp(content订单状态shipped), ]) tools { query_order: { handler: lambda args: {order_id: args[order_id], status: shipped}, } } result run_agent(fake_llm, tools, system, 查一下订单) assert shipped in result这个测试隔离了模型的不确定性。即使没有网络、没有 API Key也能验证编排层的逻辑正确模型请求工具时工具被调用返回结果被追加Agent 正常终止。4.2 跑通真实模型的最小验证流程Fake LLM 测试通过后再用真实模型跑一轮最小验证。验证场景要覆盖三类输入正常场景、需要工具的场景、工具失败的场景。python main.py预期日志如下INFO:root:step1 messages2 INFO:root:call toolquery_order args{order_id: 2025001} INFO:root:agent finished at step2 订单状态shipped看到call tool这一行说明模型确实走了原生工具调用协议而不是靠解析 JSON。如果日志里只有 request 没有 call tool说明工具注册没有生效需要检查tools参数是否传给了模型接口。4.3 对比输出、耗时和 token 消耗重构前后各跑一遍相同的用例记录三个指标最终回答是否一致、总耗时、总 token 消耗。重构后输出应当保持一致如果出现明显差异优先排查提示词改动是否影响模型决策其次是工具描述变化是否改变模型的选择倾向。token 消耗建议单独建一个统计避免输出内容过长。如果单轮 token 明显下降通常是因为提示词瘦身和上下文裁剪生效了。如果 token 反而上升要看是不是工具描述补得太详细导致模型每次调用都重复读取大段 schema。工具描述要控制粒度只写模型决策需要的信息底层字段交给 schema 校验。5. 清理过程中最常踩的坑和排查顺序5.1 Agent 运行报错的通用排查链路清理过程中遇到问题按下面的顺序排查不要一上来就怀疑模型能力。看日志。先确认 Agent 停在哪一步是模型没有返回还是工具调用失败还是编排层异常退出。看输入。打印 messages 前两条确认 system prompt 和用户输入是否符合预期。看工具注册。确认工具名、schema、handler 是否都注册成功模型返回的 tool name 能不能找到。看工具参数。打印 tool_calls 里的 args和 schema 对比看是模型多传了字段还是缺了字段。看外部依赖。工具调用的是数据库、HTTP 服务还是其他 Agent确认对方是否正常超时时间是否合理。看终止条件。确认 MAX_STEPS 是否太小导致正常的多步任务被中断。5.2 高频问题速查表结合 Agent 项目常见的线上报错整理一张速查表问题现象常见原因检查方式处理建议Agent 执行超时provider 没有及时响应模型服务限流、网络抖动、单次请求体过大看 provider 日志、网络耗时、请求 token 数增加超时和重试降低单次上下文长度Agent 执行被终止提示让模型重试或重新开始工具异常未捕获、达到 MAX_STEPS看工具日志和 step 日志工具异常改为返回 error 结果由模型继续处理模型不调用工具只回答文本工具描述不清晰或模型不支持工具调用打印 tool_calls检查 tools 是否传入精简工具描述升级支持 tool calling 的模型模型返回 JSON 而不是原生工具调用框架版本不匹配工具协议未生效检查框架文档和版本升级框架统一走原生 tool_calls 协议清理后行为变化提示词或工具描述被改动对比重构前后的 prompt 和描述每次只改一个变量跑回归测试响应越来越慢历史消息无限增长统计 messages 长度和 token 数补记忆摘要和裁剪策略5.3 清理时不要做的三类重写第一不要为了结构好看直接重写整个 Agent。工具名、参数格式、prompt 语气的细微变化都会改变模型行为重写范围越大风险越高。先拆分层再逐步替换每一步都保持可运行。第二不要把逻辑继续塞进提示词。清理完成后要防止回潮。业务规则能写在代码里就写在代码里提示词只负责模型需要理解的决策规则。prompt 膨胀会直接推高 token 成本并降低模型服从度。第三不要删掉“看起来很没用”的工具和分支。很多工具调用逻辑是历史业务沉淀下来的表面没用到实际承担了边界场景。删之前先看日志确认一段时间内没有调用记录并且通过回归测试覆盖。安全方面也要注意清理工具层时工具的日志不能记录 API Key、用户敏感信息和完整请求体工具在调用外部服务前需要校验权限对 Agent 的 prompt 注入保持防御意识用户输入不能直接拼进 system prompt。6. Agent 技术债治理为什么能成为一门专业服务6.1 需求来自持续演进的生产成本Agent 项目有一个特点它不像传统 CRUD 系统那样上线后可以稳定运行很久而是要跟着模型版本、业务规则、用户反馈持续迭代。每次迭代都在改写 prompt、增加工具、调整编排逻辑。迭代一到两个季度后原始开发者可能已经转岗新接手的人面对一堆没有文档的提示词和工具函数只能靠猜。这个现实决定了 Agent 技术债治理不是一个一次性需求而是一个持续需求。企业需要有人能独立完成体检、重构、回归验证和规范建设相当于把 Agent 仓库当作正式软件工程来维护。已经有团队把这套能力做成标准服务按“体检 重构 陪跑”的模式交付这也是标题所说“清理屎山成为一门生意”的工程背景。6.2 治理服务的标准交付物如果把这套能力作为服务交付至少要包含以下内容体检报告列出 prompt、工具、编排、记忆、可观测性五个位置的具体问题和风险等级。重构方案明确哪些是架构调整、哪些是代码修复并给出分阶段落地计划。分层后的代码结构提示词、工具、编排三层分离的可运行仓库。回归测试集至少覆盖正常回答、工具调用、工具失败、步数超限四类场景。可观测性配置结构化日志、trace_id、token 统计和关键指标面板。维护规范prompt 变更流程、工具新增流程、发布前检查清单和回滚预案。这组交付物的价值在于它把原本只存在于开发者脑子里的隐性知识变成了团队可以持续使用的工程资产。企业为这项服务付费的意愿来自维护成本下降和线上风险可控而不是单纯的代码美化。6.3 落地一套 Agent 仓库维护规范清理完屎山之后还需要一套防止回潮的维护规范。推荐固定以下检查点Prompt 变更必须走代码评审变更后跑三到五条回归用例。新增工具必须有 schema 校验、异常处理和结构化日志缺少三项不允许合入。编排循环必须有步数上限和终止条件不允许出现无边界 for 循环。会话上下文必须有裁剪或摘要策略禁止长期会话无限追加消息。所有外部服务调用必须设置超时并对超时和限流做降级。日志中敏感信息必须脱敏工具权限按最小化原则配置。发版前执行pytest回归同时用一组固定真实模型用例做行为对比。长期维护时记录每个版本模型返回变化发现漂移及时回滚并调整 prompt。这八条规范不依赖具体框架LangChain、自研 Agent、多 Agent 协作项目都可以套用。核心判断只有一个Agent 一旦进入生产就应该按照正式软件工程来管理而不是靠个人手感持续维护。清理屎山不是终点建立让屎山不再快速堆积的机制才是这项工作真正的价值。