
做 Agent 项目时很多团队会在迭代速度和技术债务之间越走越慌。功能上线很快但半年后代码里到处是硬编码的工具调用、互相嵌套的上下文判断、难以复现的模型输出异常这时候你的 Agent 项目就已经长出了一座典型的“屎山”。本文会从 Agent 工程化角度把这种“屎山”从何而来、如何系统清理、清理过程能沉淀出什么产品和服务完整拆一遍。1. Agent 项目里的“屎山”从哪来1.1 什么是 Agent 屎山“屎山”这个词在传统软件开发里很常见指的是那些没有清晰结构、充满临时补丁、改动一处就崩三处、只能靠老员工口口相传才能维护的代码。放到 Agent 开发里它的表现形式更隐蔽系统 Prompt 动辄上千字但没人知道哪一句话是关键。工具函数散落在多个文件里同一个逻辑被复制粘贴了四五次。Agent 的循环逻辑里塞满了状态变量和 if/else跑一次后状态就乱了。模型偶尔返回错误格式代码就用字符串匹配硬撑。多个 Agent 之间互相调用但链路完全无法追踪。传统屎山一般在代码逻辑层而 Agent 屎山是“代码逻辑 Prompt 文本 模型行为 工具参数”共同堆出来的。它更难清理因为你不仅要重构代码还要处理模型输出的不确定性。1.2 为什么 Agent 代码容易烂这里必须承认 Agent 开发本身的模式有问题。大多数团队是从“跑通 demo”开始的从 LangChain 等框架里顺手拿一段模板调通一个工具就继续接下一个功能。这种快速堆叠的节奏非常适合验证 AI 能力却非常不适合长期维护。另外一个直接原因Agent 是被“对话式交互”驱动的这会让开发者下意识把业务判断写进 Prompt而不是写进代码。比如“如果用户说需要天气就调用天气工具”这种规则看起来很自然但一旦规则多了Prompt 就会变成一份无法测试、无法版本管理的“接口文档”。更麻烦的是模型返回格式不稳定。同一个 Prompt 在这个版本可能返回 JSON下个版本可能把 JSON 包在 Markdown 代码块里。为了兼容这些输出大家会写各种解析补丁这些补丁一层套一层最终就构成了 Agent 项目里最顽固的屎山。1.3 “清理 Agent 屎山”为什么能成为生意很多人以为只有大型项目才需要清理技术债但现实是现在大量中小型团队正在快速铺 Agent。他们可能花了三周上了一个内部客服机器人但没人能说清楚中间几轮工具调用到底发生了什么。再过两个月模型升级一次业务反馈变差新人接手时根本不敢动。如果这时候有人能提供一套“Agent 代码体检 分层重构 测试补全 可观测性改造”的服务帮团队把项目从不可维护变成可维护这本身就是很有价值的商业服务。它可以是独立咨询项目也可以沉淀成自动化迁移工具甚至可以做成 Agent 开发框架的脚手架。所以别再觉得“清理屎山”只是苦力活它其实是一种工程化能力输出。2. 前置认知先看清 Agent 项目的架构骨架2.1 Agent 基础概念Loop、Harness、Skill 怎么分在聊清理之前先把几个容易混淆的概念理清。很多人会在技术方案里把 Agent 的各个部分命名混乱最后屎山越堆越大。Agent Loop指 Agent 的核心循环也就是“模型思考 - 决定是否调用工具 - 得到工具结果 - 继续模型思考”的循环。这个循环决定了多轮调用的复杂度也最容易藏状态问题。Harness可以理解成运行容器或执行环境。它负责管理 Agent 的输入输出、上下文、工具调用权限以及错误恢复。有些框架中 Harness 和 Agent 是两个概念Harness 更偏执行层。Skill可以理解成 Agent 可复用的能力单元通常是一组工具或者一套特定任务处理逻辑。Skill 和 Tool 的区别是Tool 是单个动作Skill 是可以组合动作的流程。清理屎山时第一步不是改代码而是先定义清楚你的项目里循环、容器、能力分别是什么。命名混乱会直接导致维护时没人知道改哪里。2.2 常见 Agent 项目目录结构在实际项目中如果目录结构一开始就没有设计后续一定乱。以下是一个比较合理的 Agent 项目目录结构示例agent-project/ ├── agent/ │ ├── loop.py # Agent 循环逻辑 │ ├── harness.py # 执行容器与生命周期 │ ├── state.py # 状态管理 │ └── prompt_repo/ # Prompt 版本管理 │ ├── system_prompt_v1.md │ └── system_prompt_v2.md ├── skills/ │ ├── search/ │ ├── code_exec/ │ └── data_query/ ├── tools/ │ ├── registry.py # 工具注册表 │ ├── weather.py │ └── calculator.py ├── tests/ │ ├── test_loop.py │ ├── test_tools.py │ └── fixtures/ ├── logs/ └── main.py如果你的项目目前看起来是“一个 main.py 三千行Prompt 直接写在字符串变量里工具函数散落在 utils 里”那么恭喜你你找到屎山入口了。2.3 判断“屎山”的体检清单在动手重构之前可以先给自己当前的项目做一次快速体检。回答以下问题每个问题如果答案是“否”就意味着存在一块待清理的债务问题健康状态Agent 循环逻辑是否与工具调用完全分离是系统 Prompt 是否独立存放且存在版本记录是每个工具是否都有明确的输入输出 Schema是工具调用是否有统一异常处理和超时控制是多个 Agent 协作时链路是否可追踪是是否存在针对模型输出的自动回归测试是关键配置是否通过环境变量或配置中心管理是生产环境是否有完整日志和 trace 信息是如果一个项目有多项为“否”说明它正在滑向屎山状态。接下来需要按照后面的步骤分阶段清理。3. 清理第一步给 Agent 代码做结构梳理与分层3.1 分层设计触发层、调度层、能力层、存储层清理 Agent 项目核心思路是“把不可控的模型行为隔离在可控的工程结构里”。常见分层方案是四层触发层负责接收用户输入或上游事件例如 Webhook、消息队列、API 请求。调度层负责 Agent Loop决定模型如何被调用、工具如何被选择、上下文如何拼接。能力层包括 Skills 和 Tools负责具体执行动作比如查天气、查数据库、调用内部接口。存储层管理会话状态、历史消息、Prompt 文本、Agent 配置。这么做的好处是如果模型输出异常只需要在调度层做兜底如果业务接口变化只需要改能力层如果 Prompt 版本冲突只需要改存储层里的 Prompt 文件。3.2 用接口隔离模型和工具很多屎山代码最典型的问题是模型和工具直接耦合。例如def run_agent(user_input): messages [{role: system, content: SYSTEM_PROMPT}] messages.append({role: user, content: user_input}) result llm.chat(messages) if weather in result: return get_weather(city北京) if calculate in result: return calculate(expressionresult.split(:)[-1]) return result这段代码里对于“工具选择”完全靠模型输出文本里的关键词匹配工具调用和 Agent 主流程混在一起。只要新增一个工具就得改 Agent 主流程还要祈祷模型每次都输出对应的关键词。重构思路是定义统一的工具接口。from abc import ABC, abstractmethod from typing import Any, Dict class BaseTool(ABC): name: str description: str parameters: Dict[str, Any] {} abstractmethod def execute(self, **kwargs) - Any: pass所有工具都实现这个接口比如天气工具class WeatherTool(BaseTool): name weather description 查询指定城市的天气情况 parameters { type: object, properties: { city: {type: string, description: 城市名称} }, required: [city] } def execute(self, **kwargs): city kwargs.get(city) # 这里调用真实天气服务 return {city: city, temperature: 22°C}然后在调度层通过工具注册表动态查找并执行工具这样新增工具时不需要改动 Agent 循环。3.3 清理示例把散落的工具调用抽成统一 Tool接下来给出一个重构前和重构后的对比。清理前的典型垃圾代码# 旧代码main_agent.py 片段 def execute_tool(name, args): if name search: return search_svc.query(args.get(keyword, )) elif name calendar: cal calendar_svc if args.get(action) create: return cal.create(args[title], args[time]) elif args.get(action) list: return cal.list() elif name email: return email_svc.send(args[to], args[title], args[body]) else: return {error: unknown tool}虽然它用了 if/elif但只要工具达到 20 个这个函数就会变成一部长长的电话本。清理后改成注册表模式# 新代码tools/registry.py class ToolRegistry: def __init__(self): self._tools {} def register(self, tool: BaseTool): self._tools[tool.name] tool def get(self, name: str) - BaseTool: if name not in self._tools: raise KeyError(fTool [{name}] not found) return self._tools[name] def list_tools(self): return [tool.name for tool in self._tools.values()] registry ToolRegistry() # 在项目启动时统一注册 registry.register(WeatherTool()) registry.register(CalculatorTool())调度层执行工具时只需要tool registry.get(tool_name) result tool.execute(**tool_args)这样模块边界清楚了后续做权限控制、超时管理、日志埋点也能统一加。4. 清理第二步状态管理与 Prompt 治理4.1 管理对话上下文Agent 多轮对话最头疼的问题就是“上下文爆炸”和“状态丢失”。很多屎山代码直接用一个 Python 列表塞所有历史消息结果请求越来越慢、费用越来越高。合理做法是设置最大上下文窗口。定期压缩历史总结。用独立的 State 对象保存会话数据而不是让模型自己记忆。下面是一个简单的上下文管理示例class ConversationManager: def __init__(self, max_turns10): self.max_turns max_turns self.history [] self.summary def add_message(self, role, content): self.history.append({role: role, content: content}) if len(self.history) self.max_turns: self.summary self._compress_history() self.history [] def build_messages(self, system_prompt): messages [{role: system, content: system_prompt}] if self.summary: messages.append({role: system, content: 历史摘要 self.summary}) messages.extend(self.history) return messages这样上下文结构直观也方便后续接向量库存储历史摘要。4.2 Prompt 版本化清理 Agent 屎山时Prompt 是绝对不能忽略的部分。很多团队的 Prompt 直接写在代码字符串里改一处就要重新部署一次应用。更好的做法是把 Prompt 当作数据来管理。建议将 Prompt 文件独立存放并加上版本字段--- version: 2.0 created_at: 2025-01-10 changed_by: zhangsan description: 支持多工具调用的系统提示词 --- 你是一个智能助手。你可以调用以下工具 {{tools_list}} 请根据用户问题选择合适的工具。然后通过模板引擎加载from jinja2 import Template def load_prompt(versionv2): with open(fprompt_repo/system_prompt_{version}.md, encodingutf-8) as f: content f.read() return Template(content).render(tools_listregistry.list_tools())这样做之后调 Prompt 就不需要改代码版本也可以回滚。4.3 用状态机或工作流替代繁琐 if/else很多 Agent 项目里业务流程不是用代码显式控制的而是全交给模型自由发挥。这在简单场景没问题但业务复杂时会出现大量不可复现的随机行为。为了可控可以在关键业务节点引入状态机/工作流。比如一个“售前客服 Agent”可以先定义几个状态INIT - COLLECT_INFO - QUERY_PRODUCT - OFFER_PRICE - END在状态机里每一步可以指定可用的工具和强制校验字段模型只能在状态约束下调用工具。这样即使模型抽风也不会跳出业务范围。清理屎山时如果原有 Agent 有大量 if/else 判断用户意图可以逐步换成显式状态机把不稳定的“意图猜测”替换成稳定的流程控制。5. 清理第三步Agent 的测试与安全加固5.1 回归测试优先清理屎山有一个铁律先补测试再动代码。没有测试的重构等于裸奔。对 Agent 项目来说测试不能只测函数还要测“模型行为”相关的场景。常见做法是使用 Mock 数据配合确定性工具结果。一个简单的 Agent 循环测试示例import pytest from unittest.mock import Mock def test_agent_loop_with_mock_llm(): fake_llm Mock() fake_llm.chat Mock(return_value{tool: weather, args: {city: 北京}}) agent create_agent(llmfake_llm, registryregistry) result agent.run(北京天气怎么样) assert result[temperature] 22°C这里把模型返回固定 JSON验证调度层是否正确解析并调用了天气工具。只要这类测试覆盖了核心链路重构时就有底气。5.2 工具调用的权限与边界清理屎山也是安全补位的机会。很多 Agent 工具一开始没有鉴权任何用户输入都可能触发工具调用。生产环境必须做到每个工具声明自己的权限等级。Agent 执行工具前校验会话身份。敏感工具删除、写库、调用支付接口必须二次确认或强制人工审批。增加超时和重试策略防止 Agent 卡死在某个工具上。class SensitiveTool(BaseTool): require_confirm True def execute(self, **kwargs): # 实际业务逻辑 pass在调度层如果需要执行敏感工具可以先询问用户确认确认通过后再实际调用。5.3 日志追踪与可观测性清理 Agent 屎山时最容易被忽视的是可观测性。Agent 是多步调用如果日志里看不到每一步的工具参数和结果出了问题根本无法定位。推荐在 Agent Loop 的关键节点记录结构化日志。import logging import json logger logging.getLogger(agent.trace) def trace_step(step_name, input_data, output_data): logger.info(json.dumps({ event: agent_step, step: step_name, input: input_data, output: output_data, }, ensure_asciiFalse))在工具调用前后都埋点这样在排查“模型为什么连续调用五次工具”时能直接看到完整的调用链。6. 实战案例重构一个“多工具调度 Agent”6.1 原始代码典型的屎山状态现在我们把前面讲的方法综合起来看一个具体案例。假设你手上有一个多工具 Agent它需要支持查询天气、计算器、待办事项查询三个能力。原始代码如下# 旧代码main.py import json SYSTEM_PROMPT 你是一个助手请根据用户问题选择工具。 如果用户问天气返回 {tool: weather, city: 城市名} 如果用户问计算公式返回 {tool: calc, expression: 表达式} 如果用户问待办事项返回 {tool: todo, action: list} def parse_llm_output(text): # 兼容模型输出中的 markdown 代码块 if json in text: text text.split(json)[1].split()[0] return json.loads(text) def run_agent(user_input): # 调用模型 messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_input}, ] # 这里用一个模拟函数代替真实模型调用 output_text llm_chat(messages) try: parsed parse_llm_output(output_text) except Exception: return {error: 模型输出解析失败} tool_name parsed.get(tool) if tool_name weather: city parsed.get(city, 北京) return weather_api.query(city) elif tool_name calc: expr parsed.get(expression) return eval(expr) elif tool_name todo: action parsed.get(action) if action list: return todo_api.list() else: return {error: 不支持的 action} else: return {error: 未知工具}这段代码的问题非常明显工具选择和业务执行耦合在一个函数里。eval直接执行模型给出的表达式存在严重安全风险。每个工具的参数都手动获取没有统一校验。Prompt 直接硬编码无法做版本管理。完全没有日志和异常追踪。6.2 重构后的目录结构我们把项目改成如下结构agent-demo/ ├── agent/ │ ├── loop.py │ └── prompt_repo/ │ └── system_prompt_v2.md ├── tools/ │ ├── base.py │ ├── registry.py │ ├── weather.py │ ├── calculator.py │ └── todo.py ├── main.py └── tests/ └── test_agent.py6.3 核心代码统一工具接口先定义基础工具类# tools/base.py from abc import ABC, abstractmethod from typing import Any, Dict class BaseTool(ABC): name: str description: str parameters: Dict[str, Any] {} abstractmethod def execute(self, **kwargs) - Any: pass定义计算器工具注意这里不再使用eval而是使用ast安全解析# tools/calculator.py import ast import operator class CalculatorTool(BaseTool): name calculator description 计算数学表达式 parameters { type: object, properties: { expression: {type: string, description: 例如 1 2} }, required: [expression] } def execute(self, **kwargs): expr kwargs.get(expression, ) # 使用 ast 解析避免 eval 的安全问题 tree ast.parse(expr, modeeval) return self._eval_node(tree.body) def _eval_node(self, node): if isinstance(node, ast.Expression): return self._eval_node(node.body) elif isinstance(node, ast.Constant): return node.value elif isinstance(node, ast.BinOp): left self._eval_node(node.left) right self._eval_node(node.right) op { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, }.get(type(node.op)) if op is None: raise ValueError(不支持的运算符) return op(left, right) else: raise ValueError(不支持的表达式)天气工具和待办工具类似只保留核心调用。6.4 重构后的 Agent Loop# agent/loop.py import json from tools.registry import registry class Agent: def __init__(self, llm, registry): self.llm llm self.registry registry def run(self, user_input, system_prompt): messages [ {role: system, content: system_prompt}, {role: user, content: user_input}, ] response self.llm.chat(messages) parsed self._parse_tool_call(response) if error in parsed: return parsed tool_name parsed.get(name) tool_args parsed.get(arguments, {}) try: tool self.registry.get(tool_name) result tool.execute(**tool_args) return {tool: tool_name, result: result} except KeyError: return {error: f工具 {tool_name} 不存在} except Exception as e: return {error: str(e)} def _parse_tool_call(self, text): if json in text: text text.split(json)[1].split()[0] try: data json.loads(text) return { name: data.get(tool), arguments: data.get(args, {}) } except json.JSONDecodeError: return {error: 模型输出解析失败}主程序入口# main.py from agent.loop import Agent from tools.registry import registry from tools.weather import WeatherTool from tools.calculator import CalculatorTool from tools.todo import TodoTool # 注册工具 registry.register(WeatherTool()) registry.register(CalculatorTool()) registry.register(TodoTool()) # 模拟 llm def fake_llm_chat(messages): return {tool: calculator, args: {expression: 1 2 * 3}} class FakeLLM: def chat(self, messages): return fake_llm_chat(messages) if __name__ __main__: agent Agent(llmFakeLLM(), registryregistry) result agent.run(1 2 * 3 是多少, system_prompt你是一个 Agent) print(result)6.5 运行验证与前后对比运行python main.py预期输出{tool: calculator, result: 7}重构后工具选择、执行、解析分离。新增工具只需要在registry注册不需要改Agent类。计算器不再使用eval安全风险降低。Prompt 可以独立管理。后续接日志、权限验证时可以在Agent.run的统一入口加逻辑。这个案例比较小但核心思路是通用的。真正的 Agent 屎山项目清理就是把一个巨大的“毛线团”拆成清晰的模块让每一段逻辑独立可测。7. 常见问题与排查思路清理 Agent 屎山过程中经常会遇到下面这些问题问题现象常见原因解决思路Agent 循环中状态丢失上下文拼接或状态对象设计不合理引入独立的 State 对象统一管理会话上下文模型偶尔输出非 JSON 格式模型随机性或 Prompt 约束不足增加输出解析兜底例如用正则提取 JSON 或启用结构化输出新增工具后主流程被改坏工具调用和主流程强耦合迁移到工具注册表模式Prompt 改动不可追踪Prompt 硬编码在代码中把 Prompt 独立成文件并加入版本字段工具接口发生变更影响多个 Agent缺少接口抽象定义 BaseTool 基类统一参数和返回值重构后行为不一致缺少回归测试先写测试再重构使用 Mock 固定模型输出Agent 执行敏感操作工具权限边界不清给工具加权限等级敏感操作需二次确认生产环境无法排查调用链日志缺失在 Agent Loop 和工具调用处埋结构化日志排查技巧遇到问题时先从日志看模型输出了什么再看解析层有没有吞掉错误最后看工具执行报错。这三步可以覆盖大部分 Agent 运行异常。8. 从“清理”到“预防”工程化最佳实践清理一次屎山不算本事让 Agent 项目后续不重新变烂才算。这里给出几条工程建议。8.1 命名与分层规范Agent、Harness、Skill、Tool 这些术语在项目里要统一不能用“那个大模型的东西”“服务那边的方法”来代指。目录结构按“调度 / 能力 / 存储 / 测试”划分业务代码不要写在入口脚本里。工具命名统一用动词或领域名词例如weather_query、order_cancel。8.2 配置和密钥管理模型 API Key、服务地址、模型名称等配置不允许硬编码。至少使用环境变量管理本地配置生产环境可以用配置中心。敏感工具的开关和权限建议做成配置项不要在代码里写死。8.3 异常处理与重试Agent 工具的异常必须被捕获并返回给上层而不是让模型输出一个Tool execution error。对于偶发超时和网络抖动可以加指数退避重试策略但要设置最大次数避免 Agent 卡死。模型输出异常时要保留原始输出方便排查。8.4 日志与可观测性每条日志至少包含时间、会话 ID、Agent 名称、步骤名称、耗时、结果。工具调用日志必须记录参数和返回值但要注意脱敏。生产环境建议接入链路追踪系统将 Agent 调用链和分析工具串起来。8.5 测试策略单元测试覆盖工具自身逻辑。Agent 回归测试使用 Mock 模型固定输出验证调度结果。集成测试真实调用模型但使用模拟外部服务验证端到端链路。上线前至少保证核心路径通过回归测试。8.6 多 Agent 协作时的规范多个 Agent 互调时屎山会指数级增长。建议所有 Agent 都通过统一网关或事件总线通信不要在代码里硬编码另一个 Agent 的类或接口。每个 Agent 需要有明确的输入输出定义且配置独立部署。如果两个 Agent 开始互相 import就要立刻拆开。9. 商业化机会说人话最后聊聊“赚钱”这件事但这里的“赚钱”不是说推荐某个产品而是分享一些可落地的思路。9.1 技术咨询与重构服务这是最容易切入的方向。很多企业已经上线了内部 Agent但没人能动它。你可以提供“Agent 代码体检 重构方案 分阶段改造”的咨询服务按次收费或者按项目收费。体检报告可以包括代码结构健康度。Prompt 版本管理情况。工具调用耦合度。测试覆盖率。生产环境可观测性。然后按问题优先级给出改造计划第一阶段先做安全加固和日志第二阶段做工具解耦第三阶段做状态治理。这种服务对团队的价值非常直接。9.2 企业内训与规范落地清理屎山的过程本身就是一次团队能力提升。很多团队需要有人帮他们建立 Agent 开发规范和代码评审标准。你可以把前面提到的分层、注册表模式、测试策略、Prompt 版本管理等内容做成培训课程边讲边带着改写当前项目。这种方式虽然看起来没有产品化但复购率很高。因为只要团队继续做 Agent就会持续需要规范迭代。9.3 自动化重构工具与 CLI如果你本身是开发者可以考虑把你清理屎山的经验沉淀成 CLI 工具。比如agent-scan扫描项目目录分析 Agent 代码结构输出健康报告。agent-refactor自动把散落的工具调用迁移到注册表模式。prompt-version管理 Prompt 的版本和灰度。这类工具可以开源一部分再对高级功能收费。相比纯咨询服务工具的边际成本更低即使单个用户定价不高也能跑出规模。9.4 独立开发者机会更轻量的做法是做模板和脚手架。现在很多人第一次做 Agent缺的就是一套合理结构的参考实现。你完全可以做一个“可维护 Agent 模板”内置工具注册表、状态管理、Prompt 版本化和测试示例以付费模板或赞助形式分发。关键是“清理屎山”服务不只是体力活它需要大量的真实项目经验。你能见到越多的坏味道越能总结出高效的清理套路。这些经验本身就是壁垒。如果你现在正好在维护一个日益膨胀的 Agent 项目可以先把目录结构和工具调用梳理一遍然后补上回归测试再逐步把 Prompt 外置。这个过程不会让你立刻发财但它能帮你建立一套通用的 Agent 工程化能力而这种能力无论用来做产品还是做服务都很有空间。