1. 从“修水管”视频到“自工程完结”一个被忽视的工程铁律前几天我在一个程序员社区里刷到一个挺火的短视频标题大概是“程序员修水管结果修出个更大的Bug”。视频里一个哥们儿家里的水管漏水他手忙脚乱地拧紧了某个阀门结果水是不漏了但整个楼层的供水压力都出了问题邻居家也跟着遭殃。评论区里一片“哈哈哈这很程序员”但笑过之后我心里却咯噔一下。这不就是我们日常开发中尤其是现在搞AI Agent这类复杂系统时每天都在上演的“经典戏码”吗为了快速解决眼前的一个报错比如某个API调用失败我们可能随手加个补丁、绕个弯子代码是能跑了却把更隐蔽、更棘手的问题比如资源泄漏、状态不一致像“水压异常”一样悄无声息地传递给了下游模块甚至是最终用户。这个场景完美地诠释了TPS丰田生产方式里一个核心到不能再核心的理念——“自工程完结”。最近我在深度参与一个名为Agent Harness的开源项目时对这个理念有了近乎“刻骨铭心”的体会。Agent Harness你可以把它理解为一套专门为AI Agent打造的“脚手架”或“基础设施层”。它不负责替代Agent本身的核心推理逻辑那是LLM和具体技能模块的事而是专注于解决那些让Agent能稳定、可靠、可观测地运行起来的“脏活累活”比如生命周期管理、工具调用编排、状态持久化、异常处理和监控等等。那么“自工程完结”到底是什么意思简单粗暴地讲就是**“别把Bug或者任何形式的问题、半成品留给下一道工序”**。在制造业的生产线上这意味着每个工位的工人必须确保自己经手的零件是100%合格的才能流到下一个工位。在软件工程特别是我们正在构建的AI Agent系统中这意味着每一个模块、每一次函数调用、每一个数据转换都必须在其边界内尽最大可能处理掉所有它能预见的异常和问题输出一个确定、可靠的结果给它的调用者而不是简单地抛出一个错误或者传递一个模棱两可的状态。这听起来像是常识对吧但现实是在追求快速迭代、功能上线的压力下我们太容易妥协了。“先让流程跑通回头再优化”、“这个异常概率很低先不管了”、“让调用方去处理吧”……这些想法就是“Bug流水线”的开端。Agent Harness项目让我意识到对于AI Agent这种高度动态、依赖外部服务、状态复杂的系统如果不贯彻“自工程完结”整个系统就会像一个到处漏水的管道网络运维成本和调试难度会呈指数级上升。今天我就结合Agent Harness开发中的具体案例跟你聊聊为什么这个来自传统制造业的理念在AI时代反而成了我们最该捡起来的“工程利器”。2. 拆解“自工程完结”它远不止是“不要抛出异常”很多人一听到“别把Bug留给下一道工序”第一反应就是“哦就是要多做点异常捕获别乱抛异常”。这理解得太浅了。在Agent Harness的语境下“自工程完结”有着更丰富、更严格的内涵。我们可以把它分解为三个递进的层次这恰恰对应了构建稳健AI Agent基础设施的三个关键维度。2.1 第一层数据与状态的“完结”这是最基础的一层。任何一个函数或模块对于其输入必须有明确的契约和验证对于其输出必须有确定的格式和状态。反面案例在早期版本的Agent Harness中我们有一个工具调用路由模块。它的职责是根据Agent的意图将调用分发给不同的工具Tool。最初的设计是如果找不到匹配的工具它就返回一个None。问题来了调用方拿到None后该怎么办是重试是记录日志后跳过还是向上抛出“工具未找到”错误每个调用方都要重复实现这套逻辑而且极易出错。更糟糕的是有些工具调用后返回的数据结构不一致有的返回字典有的返回字符串有的在出错时返回一个包含error字段的字典。路由模块只是简单地透传这些结果。“自工程完结”的改造输入完结路由模块强制要求所有注册的工具必须提供统一的元数据描述名称、描述、输入参数schema。在路由前先根据schema校验输入参数的合法性和完整性不合法则立即在此模块内反馈“参数错误”并附上具体原因而不是把原始参数丢给工具去报错。输出完结我们定义了一个统一的ToolResponse对象。无论工具内部执行成功与否路由模块都必须返回这个对象。这个对象包含几个关键字段success: bool(成功/失败)data: Any(成功时的标准化数据如将不同工具的原始输出转换为一个标准JSON结构)error: ToolError(失败时的错误对象包含错误码、错误信息、可选的原始异常)tool_name: str(调用的工具名)execution_id: str(本次执行的唯一ID用于链路追踪)这样调用方通常是Agent的核心逻辑拿到ToolResponse后只需要检查success字段就能做出确定性的下一步决策。数据的形态和状态的语义在路由模块这一“工序”就完结了。注意这里“完结”的不是可能性工具仍然可能因为网络、权限等问题失败而是信息表达的完备性和一致性。调用方无需猜测结果的含义。2.2 第二层逻辑与流程的“完结”这一层关注的是业务流程或控制流在一个模块内的完整性。一个模块应该尽可能处理其业务逻辑范围内的所有分支避免将流程控制的复杂度暴露给上游。反面案例考虑Agent的“记忆”或“状态持久化”模块。假设Agent执行到一半崩溃了重启后需要恢复状态。最初的简单实现是提供一个save_state(state)和load_state(agent_id)函数。如果load_state时找不到数据就返回None。于是Agent的启动逻辑里就需要写一堆if-elseif state is None: 初始化新会话 else: 恢复旧会话。这看起来没问题但把“状态不存在时应初始化”这个业务决策从存储模块剥离到了核心逻辑模块。如果未来我们想改变策略比如状态不存在时从备份中加载就需要修改核心逻辑。“自工程完结”的改造 我们重构了状态管理模块提供一个get_or_initialize_state(agent_id, initial_state_factory)方法。这个方法内部封装了完整的逻辑尝试加载agent_id对应的状态。如果加载成功直接返回。如果加载失败文件不存在、数据损坏等它不会简单返回None而是会 a. 首先尝试记录错误和进行可能的修复如读取损坏文件的备份。 b. 如果最终确定状态无法恢复则调用传入的initial_state_factory函数创建一个新的初始状态。 c.可选但强烈推荐将这个新创建的状态立即持久化并记录一条“状态初始化”的审计日志。这样对于Agent的核心逻辑来说它调用get_or_initialize_state后一定能拿到一个有效的、立即可用的状态对象。至于这个状态是旧的、新的、还是修复后的核心逻辑不关心也无需包含相关判断代码。整个“获取可用状态”的流程在状态管理模块内部就“完结”了。2.3 第三层资源与副作用的“完结”这是最高级也最容易出问题的一层。它要求一个模块必须管理好它申请的所有资源网络连接、文件句柄、内存、外部服务会话等并确保其操作产生的副作用如修改了全局配置、发送了通知在模块边界内是可预测和可清理的。反面案例经典Bug在集成一个外部知识库查询工具时该工具需要维护一个昂贵的数据库连接池。最初的工具实现提供了一个query(sql)方法但连接池的初始化(init_pool)和关闭(close_pool)需要调用方显式管理。于是Agent的生命周期管理代码变得复杂要在合适的时候init_pool更要在Agent停止时或在异常发生时记得调用close_pool。一旦忘记就会导致连接泄漏。这就像打开了水龙头没关把“关水”的责任留给了房子里的下一个人。“自工程完结”的改造 我们利用Agent Harness提供的生命周期钩子Lifecycle Hooks和上下文管理器Context Manager模式对这类工具进行了重构。工具自身实现资源管理工具类实现__enter__和__exit__方法或者提供start()/stop()方法。在__enter__/start()中初始化连接池在__exit__/stop()中确保关闭所有连接。Harness框架负责调度Agent Harness在加载该工具时会识别其生命周期接口。当Agent启动时Harness自动调用工具的start()当Agent停止无论是正常结束还是因异常崩溃Harness都会保证调用工具的stop()进行清理。对调用方透明Agent的核心逻辑在调用query(sql)时完全无需关心连接池是否存在、是否健康。它假设工具在任何时候都是“就绪”的。资源管理的责任从分散的、易忘的调用方收归到了工具模块内部和Harness框架这个统一的“工序”中。这个改造直接避免了类似isAutoCloseConnection false这种配置错误导致的Bug。因为关闭连接不再是可选的而是框架强制保障的、在“工具使用”这个工序内必须完结的操作。3. Agent Harness如何将“完结”理念工程化理解了理念我们来看看在Agent Harness这个具体项目中是如何通过设计和约定将“自工程完结”从口号落地的。这不仅仅是代码风格更是一套嵌入到框架DNA里的约束和最佳实践。3.1 通过强类型与契约定义边界松散的类型是Bug的温床。Agent Harness广泛使用像Pydantic这样的数据验证库为几乎所有在模块间传递的数据结构定义严格的Schema。工具输入/输出契约每个工具都必须声明其输入参数的JSON Schema和输出数据的类型。Harness在调用前进行校验不符合契约的请求根本不会到达工具逻辑。这确保了工具接收到的输入是“完结”的格式正确、类型匹配。Agent状态契约Agent的长期记忆和短期状态也被要求定义为一个Pydantic模型。任何对状态的读写都经过模型的序列化和反序列化自动过滤掉非法字段保证状态结构的完整性。当状态从一个会话持久化到另一个会话时其结构是确定无疑的。消息流契约Agent与用户、Agent与工具之间的消息如ChatMessage, ToolCallMessage, ToolResultMessage都有明确的类型定义。这保证了在复杂的多轮对话和工具调用流水线中每个环节处理的数据对象都是已知的、可靠的。这种强类型约束就像给每个工序的输入输出都配备了标准的“夹具”和“量具”不合格的零件根本无法流入下一站。3.2 统一的错误处理与降级策略“完结”不是不允许出错而是要求错误必须在当前环节被妥善封装和处理并以一种对下游友好的方式呈现。Agent Harness定义了一个分层的错误体系ToolExecutionError工具执行过程中发生的错误如网络超时、API限额用完。工具本身应尽可能进行重试等补救措施。如果最终失败它必须抛出一个信息丰富的ToolExecutionError而不是原始的requests.exceptions.Timeout。AgentRuntimeErrorAgent核心逻辑运行时的错误如状态机进入非法状态、决策逻辑矛盾。这部分错误通常意味着Agent逻辑有Bug需要开发介入。HarnessFatalError基础设施层面的严重错误如配置加载失败、关键资源无法初始化。这类错误通常会导致整个Agent实例无法启动。关键在于Harness提供了一个全局的错误处理与转换中间件。任何未捕获的异常都会被这个中间件捕获并尝试转换为对用户友好的消息或者触发预定义的降级策略例如当知识库查询失败时自动降级为仅使用LLM的内部知识生成回答并记录告警。这样即使最底层的模块出了错传到用户界面或上游调度系统的也是一个被“完结”处理过的、有明确后续行动建议的结果而不是一串令人崩溃的堆栈跟踪。3.3 可观测性作为“完结”的检验标准你怎么知道一个工序真的“完结”了在制造业靠质检。在软件里靠可观测性Observability。Agent Harness内置了强大的日志、指标Metrics和追踪Tracing能力这不是为了炫技而是为了给“自工程完结”提供验证手段。链路追踪Tracing为每一次用户请求、每一个工具调用、甚至每一次LLM交互生成唯一的追踪ID。你可以清晰地看到一个请求的生命周期精确定位到是哪个“工序”慢了、失败了或者输出了异常数据。如果某个模块声称自己处理完了但追踪链在此断掉或出现了非预期的跳转那它就没真正“完结”。结构化日志日志不是简单的print而是带有级别、模块名、执行ID、关键上下文的结构化数据。例如工具模块在返回ToolResponse时无论成功失败都必须记录一条包含execution_id、tool_name、duration和success字段的日志。这相当于该工序的“加工记录”可供后续审计和分析。健康检查与就绪探针每个管理资源的模块如数据库连接池、外部服务客户端都需要提供health_check()方法。Harness会定期调用或在关键操作前调用。如果健康检查失败该模块会被标记为“不健康”Harness可以阻止请求流入或切换到备用模块。这确保了只有真正“就绪”即资源层面已完结的模块才会被使用。通过这些可观测性手段我们不仅能发现问题更能量化每个模块“完结”的质量比如工具调用的平均耗时、错误率、状态恢复的成功率等为持续改进提供数据支撑。4. 实战推演从“Bug流水线”到“完结流水线”让我们通过一个更具体的、融合了多个热词的场景来看看贯彻“自工程完结”前后的巨大差异。假设我们正在开发一个“能碳管理AI Agent”它需要查询数据库、调用外部API进行计算并生成报告。场景Agent收到用户请求“计算我司上季度碳排放总量”。“Bug流水线”模式旧做法Agent核心逻辑解析用户意图生成一个查询对象{query: “碳排放总量”, period: “last_quarter”, company: “my_company”}调用“数据查询工具”。数据查询工具未完结接收查询对象发现没有company_id字段只有company名字。它尝试用名字去查一个映射表但映射表连接失败网络抖动。工具内部捕获了连接异常但只是记录了一条“映射表连接失败”的日志然后返回了None。Agent核心逻辑收到None它判断为“查询无结果”于是决定调用“估算模型工具”进行估算。估算模型工具未完结它需要company_id和period作为输入现在只有period。它尝试使用一个默认的company_id但这个默认ID在模型里没有对应数据。模型计算过程出现除零错误抛出一个原始的ZeroDivisionError。Agent核心逻辑没有处理ZeroDivisionError的代码异常向上抛出。最终结果用户看到“内部服务器错误”。开发者排查需要从最顶层的错误日志开始逆向追踪经过估算工具、Agent逻辑最后才发现根源是第一个工具里的映射表连接问题。排查链路长责任不清。“完结流水线”模式Agent Harness加持下的新做法Agent核心逻辑解析意图生成查询对象。在调用工具前Harness的输入验证中间件会根据“数据查询工具”注册的Schema自动校验对象。发现缺少company_id但多了一个company它可能会尝试补全或直接在此处返回一个验证错误给用户“请提供公司ID”。假设验证通过。数据查询工具已完结映射表连接失败。工具内部进行最多3次重试。重试均失败后它不会返回None而是构造一个明确的ToolResponse:success: falseerror: ToolExecutionError(codeDEPENDENCY_UNAVAILABLE, message公司名称映射服务暂时不可用, details{retry_attempts: 3})data: null同时它通过Harness的日志系统记录一条结构化错误日志包含错误码、重试次数和追踪ID。Agent核心逻辑收到ToolResponse检查success为false并读取error.code。它配置了错误处理策略对于DEPENDENCY_UNAVAILABLE错误触发降级策略——改为使用一个本地的、可能过时的静态映射文件来获取company_id。如果本地文件也没有则在此处决定流程的终结向用户返回一个友好的消息“暂时无法获取精确的公司信息请稍后再试或直接提供公司ID”。流程在此明确结束不会进入不可控的估算环节。可观测性整个过程的追踪链是完整的。运维人员可以在仪表板上看到请求失败在“数据查询工具”错误原因是依赖服务不可用并且看到了重试记录。Agent核心逻辑的降级决策也被记录。责任清晰定位迅速。对比之下高下立判。“完结流水线”模式下问题在最早可能的地方被识别、封装、并给出了明确的后续路径成功、失败但友好提示、触发降级。Bug没有像击鼓传花一样往下流而是在每个环节都被“消化”或“转化”了。5. 开发者的思维转变从“实现功能”到“完结工序”推行“自工程完结”最难的不是技术而是思维习惯的转变。它要求我们以终为始从“工序输出”的角度来设计每一个模块。设计时自问我这个模块/函数/类交付给调用方的“产品”是什么一个确定的数据结构一个保证清理的资源句柄一个完整的业务流程结果调用方需要做什么样的检查才能使用它我能让这个检查变得更简单甚至不需要吗实现时牢记异常不是用来抛的是用来处理的。处理不了所有异常那就处理你能处理的然后把剩下的包装成调用方能够理解和应对的类型。资源不是用来申请的是用来管理生命周期的。谁申请谁就在其作用域内负责释放。测试时验证不仅要测试正常路径更要测试边界和异常路径。你的模块在输入非法、依赖失效、资源不足的情况下输出是否依然符合“完结”的契约它会不会把烂摊子丢出去评审时关注代码评审时除了看逻辑是否正确要特别关注模块的“接口契约”和“错误处理”。看看有没有把本该自己处理的判断丢给了调用方有没有“偷偷”改变了某些全局状态而没有交代。在Agent Harness社区里我们经常互相Review代码一个常见的评论就是“这个地方返回null/undefined/异常调用方不好处理能不能在你的模块里就给它一个确定的结果” 或者 “这个资源只在try块里开了有没有在finally块或者用with语句确保关闭”这个过程一开始会有点痛苦感觉像是给自己戴上了“枷锁”。但当你习惯之后你会发现你写的代码模块性更强依赖更清晰在复杂系统比如由多个AI Agent协作的场景中集成时调试和维护的难度会大大降低。你不再需要深入每一个下游模块去理解它可能抛出的各种奇怪异常因为你信任它交给你的总是一个“完结”的、可预测的结果。6. 超越Agent通用软件工程中的“完结”实践虽然我们以AI Agent和Agent Harness为例但“自工程完结”的理念适用于几乎所有软件工程领域。微服务架构每个微服务都应该对其领域内的业务逻辑和数据做到“完结”。它应该通过API网关或服务网格处理好认证、限流、熔断对外提供稳定的接口。一个服务内部的数据存储或缓存挂了应该通过降级策略如返回缓存旧数据或默认值来响应而不是让调用方看到“数据库连接失败”。前端开发一个UI组件应该自己管理好它的加载状态、错误状态。数据获取失败时它应该在组件内部显示错误提示而不是抛出一个异常让整个页面崩溃。它接收的props应该有明确的PropTypes或TypeScript接口定义对不合理的输入进行早期警告或使用默认值。数据处理管道ETL抽取、转换、加载中的每一个步骤都应该对数据的质量负责。清洗步骤应该处理缺失值、异常值转换步骤应该保证输出格式的严格一致。一个步骤失败整个管道应该有检查点Checkpoint和重试或死信队列Dead Letter Queue机制而不是让错误数据污染下游。其核心思想是一致的通过强化模块内的内聚性和对外的契约性降低模块间的耦合度与认知负担从而构建出更加健壮、可维护的系统。在当今系统越来越复杂、依赖越来越多的环境下这种思维不再是“好习惯”而是“生存必需”。回到开头的“修水管”视频。那个程序员犯的错就是把“止住自家漏水”这个局部问题“解决”了却没有考虑这个操作在整个供水系统全局中是否“完结”。他关闭的阀门可能影响了主管道的压力平衡把问题传递给了整个系统。我们的代码也是如此。每一次图省事的try-catch后直接throw每一次返回含义模糊的null每一次忘记关闭的连接都是在给系统的“下一道工序”——可能是另一个模块可能是运维同事也可能是最终用户——埋下一颗颗定时炸弹。Agent Harness项目给我的最大启示就是它把TPS这种经过时间考验的、朴素的工程智慧用现代软件框架的形式固化了下来。它逼着我们去思考边界、契约和状态让构建可靠的AI Agent不再仅仅依赖于LLM的强大更依赖于扎实的、可预测的工程基础设施。所以下次当你写下一行代码时不妨问问自己我这个“工序”“完结”了吗