AI Agent架构设计:从单次LLM调用到稳定Harness Runtime
发布时间:2026/10/13 4:47:04 锦皓数字建站

前阵子某个同行问我AI Agent架构到底怎么选他们团队已经有一个能用的Demo但那套代码我扫了一眼本质上就是一次带上工具的LLM调用加一个while循环被包装成了Agent。我告诉他单次调用和完整Harness Runtime之间的差距不是多写几轮循环的问题而是流量一上来、任务一乱你就会发现缺的是对整个执行过程的控制。这篇文章不谈模型效果只聊从一次LLM调用到稳定Harness Runtime的架构设计与落地经验适合正在做Agent工程化、或者对现有方案不满意的开发者参考。1. 核心思路拆解单次LLM调用与Agent的边界在哪1.1 一次LLM调用的完整闭环长什么样先看最朴素形态。假设你要让模型帮你查一个数据库里的订单状态代码大概是构造一个system prompt告诉模型“你是一个订单助手可以调用查询工具”在请求里带上tools参数里面放一个查询订单状态的function描述用户问“订单A现在到哪一步了”模型返回的不是“我帮你查了是已发货”而是一个tool_call请求JSON里指明要调用query_order参数是{order_id: A}到这里你的服务端才真正接手解析出函数名和参数调用真实的数据库查询接口拿结果后作为tool message回填给模型。模型看到查询结果决定是继续调另外一个工具还是直接给出最终答案。这个闭环看似简单但我见过不少团队把这一套叫做“Agent”。严格来说这叫工具调用是Agent的最小能力单元。它解决的问题是让模型不再只能靠训练数据里的知识回答而能触达实时数据、内部系统、外部API。这一步的架构决策基本围绕协议和序列化展开比如tools的JSON Schema怎么写、arguments用字符串还是结构化对象、返回结果怎么截断都属于这一层。1.2 从单次调用到多轮循环Agent的公式单次调用本质上是“补全”模型只做一步推断。但真实任务几乎没有一步能完成。比如用户说“帮我对比这三家供应商的报价并给出推荐”模型需要先逐个查供应商报价可能还要查历史合作记录最后才能给出分析。这就必须有一个循环初始输入 - 模型推断 - 如果是工具调用执行工具 - 回填结果 - 再次交给模型 - 循环直到模型输出最终答案。这个循环看似无脑真正的复杂度在控制。我见过最典型的翻车现场某个团队把循环写成while True结果模型在一个问题上反复调同一个工具或者说“让我再确认一下”足足跑了四十多步最后账单出来远超预算。所以一个合格的Agent循环至少要有最大步数限制、单步超时、累计预算控制。当这些控制逻辑开始出现你实际上已经在写运行时的雏形了。我习惯把这层东西叫Harness Runtime它的职责不是帮你写prompt而是负责Agent执行过程中所有跟非确定性斗争的事情解析、路由、重试、终止、成本、观测。1.3 容易混淆的两层应用层与运行时层很多项目烂不是烂在模型选得不好而是把运行时逻辑写进了业务代码。你经常能看到业务类里直接写“如果模型返回tool_call就调用这个函数”函数里又穿插着上下文截断、重试逻辑、日志埋点。表面上都是代码实际上两层混在一起。我的划分标准很简单应用层管“做什么”比如查订单、写周报、生成图表这些逻辑随业务变化应该由具体Agent实现运行时层管“怎么稳定地做完”比如工具怎么注册和发现、上下文怎么管理、调用的结果如何回填、出错怎么重试、调用链路怎么追踪打个比方应用层是外卖订单里的菜运行时层是骑手的电动车、导航路线和调度系统。菜可以天天换但电动车和调度系统要稳定否则单量一大必然爆。运行时层一旦跟业务耦合每接一个新场景都要把循环重写一遍这不是架构是堆代码。2. Agent核心机制拆解工具注册、记忆管理与执行循环2.1 工具注册表不是函数列表那么简单我看到不少初版实现是把工具直接写成一个Python函数数组循环里调用func(args)完事。这在Demo阶段没问题但到生产环境工具注册表至少要承担四件事。第一是Schema集中管理。模型需要知道有哪些工具、参数是什么所以每个工具都必须有name、description、parameters的JSON Schema。description写得清不清晰直接决定模型选工具的准确率。我见过一个团队把description写得含糊模型经常选错工具后来把描述改成带正反例的详细文本选错率直接降了一半。第二是权限校验。不是所有工具都允许所有用户调用。订单查询可能只允许管理员内部数据接口不能暴露给外部Agent。这些拦截应该统一放在注册表层而不是散落在各个工具实现里。否则你排查“为什么某个用户能查到不该看的数据”时会痛苦得想重写整个系统。第三是参数转换。模型返回的arguments是JSON字符串工具函数不一定接受这种格式需要做类型校验、默认值补全、非法值清洗。有条件的可以引入Pydantic这类校验库没条件的也得手写一个集中转换器否则每个工具开头都有一段重复的“try json.loads”代码迟早会出错。第四是返回结果标准化。工具返回的可能是一个对象、一个列表、一段文本甚至一个异常。注册表最好统一包装成ToolResult结构包含状态、摘要、耗时、需要回填给模型的内容。我一开始没做标准化结果有的工具异常抛出来直接让整个Agent循环崩溃后来统一成ToolResult并规定异常也要作为正常回填内容传给模型系统才稳定下来。2.2 记忆管理上下文窗口限制下的三层策略把历史消息一股脑全塞给模型是最常见也最容易出问题的做法。模型上下文窗口有限而且窗口内塞得越满模型对关键信息的注意力越容易被稀释响应延迟和成本也跟着涨。我习惯把记忆分成三层。第一层是短期上下文最近几轮的完整消息。这一层保证模型对话连贯能知道用户上一句问了什么、当前要解决什么问题。第二层是中期摘要当对话超过一定轮数把更早的内容压缩成一段摘要放进上下文。摘要可以由模型生成也可以按规则抽取关键字段。这个压缩动作什么时候触发要根据你的场景摸索。我在一个客服类项目里定为每四轮做一次摘要效果不错。第三层是长期记忆把用户偏好、历史订单、长期目标这类信息存到外部存储比如向量库或结构化数据库。这个不能全放进上下文只在整个任务需要时做一次检索把相关结果注入。这三层设计的原因很简单模型上下文本质上是一个优先级队列永远应该装最必要的信息。很多人问“模型不是支持200K上下文吗直接全都给不就行了”实际上200K的上下文延迟和成本对你的系统压力会非常大而且信息太多时模型反而抓不住重点。真正生产级的方案永远是截断、摘要、检索三件套。2.3 执行循环从盲目重复到可控复用执行循环是Harness Runtime的心脏。它的基本流程是接收用户请求 - 调用模型 - 检查模型输出 - 如果是最终答案则结束如果是工具调用则执行工具 - 回填结果 - 继续循环。但生产环境里这个循环里隐藏着大量细节。比如你定义了几个状态idle空闲、thinking模型推理中、awaiting_tool等待工具返回、done完成、error出错。状态流转清晰以后你才能判断一个Agent现在到底卡在哪一步而不是只看日志里一堆模型token记录。循环里的解析步骤尤其重要。模型输出并不总是规规矩矩的JSON有时候它会多说一句话、带一段Markdown甚至把arguments写成非法JSON。解析器必须能提取出真正的tool_call并在解析失败时做修复和重试。我在实现中至少做了两层解析先提取JSON块再用json库解析解析失败再尝试去掉首尾的不规范字符。另一个细节是最大步数和终止条件。步数上限可以设为动态值简单任务10步复杂分析任务20步但要防止模型鬼打墙式地反复调用同一工具。我总会在循环里加一个计数map对同一个工具连续调用超过两次时就给模型提示“该工具结果已获取请直接基于现有信息回答”能有效抑制无效循环。3. 从Agent到Harness Runtime生产级基础设施问题3.1 为什么需要Runtime而不是继续堆框架很多人问市面上已经有那么多Agent编排框架为什么还要自己搞Runtime。我的看法是框架在快速验证阶段很有价值它帮你封装好了模型调用、工具调用、内置记忆这些抽象让你两小时能跑通Demo。但到了生产环境你会发现框架的抽象是通用的你的场景是特殊的。比如框架默认的循环逻辑未必支持你的定制终止条件框架内置的记忆策略未必适合你的上下文压缩节奏框架的日志格式未必能对接你已有的监控系统。当这些问题一个个出现时你面临两个选择深入框架源码去定制或者直接自研一个轻量Runtime。两个选择都成立取决于团队规模和项目复杂度但无论选哪条你都必须理解Runtime内部到底发生了什么。这就是为什么我建议所有Agent项目至少在内部把核心循环自己实现一遍哪怕只是最小几十行代码也能让你之后排查问题快得多。3.2 状态机与任务编排给Agent建立可中断的执行模型Agent一旦跑起来整个生命周期不只是“开始-结束”那么简单。真实场景里有超时、人工审核、依赖等待、并行执行这些分支。状态机是处理这些分支最直观的方式。我用一个内部模拟项目做过完整的Agent状态机Agent任务从pending开始进入running之后在thinking和tool_execution之间来回切换最终走到completed或者failed。如果中间需要人工确认就切到waiting_for_approval等外部回调恢复。这个状态机带来的最大收益是可控性。比如某一次任务慢得像蜗牛运营同学可以直接在后台看到任务停在了waiting_for_approval而不是只能干等一个神秘黑盒。状态机的实现不复杂用枚举加一个简单的状态流转判断即可复杂场景可以引状态图库但至少内部数据模型要清晰。3.3 并发调度与工具限流Agent多了以后必然踩的坑刚开始做Agent一个用户一个任务循环里直接调工具没问题。但Agent服务一旦成为公共能力多个用户同时发起任务你的工具接口就变成共享资源了。数据库连接、第三方API配额、内部服务的QPS每一项都可能被打爆。我这里踩过一个很实在的坑某次上线后Agent频繁调用一个内部查询服务结果QPS超标把那个服务直接拖挂了。后来加上两层控制才稳住。第一层是Agent级并发控制每个任务最多同时执行的工具调用数做限制比如一个任务最多2个并发工具第二层是工具级限流每个工具单独配一个信号量和超时超过限制的请求直接排队或失败回退。信号量机制在异步Python里有asyncio.Semaphore可以直接用简单配置一下就够。对工具返回过慢的情况统一设置单次工具调用的超时时间比如15秒超时就给模型回填“工具超时请换个方式处理”这比干等一个不可控的接口要靠谱得多。3.4 可观测性把Agent当分布式系统来追踪Agent循环里每一步都在跟外部系统打交道只要其中一步出了问题排查链条就会特别长。所以从第一天做Runtime就该在关键节点埋点。我常用的采集维度是每次模型调用的token数、耗时、成本每次工具调用的名称、参数、返回状态、耗时以及整个任务从开始到结束的状态流转序列。把这些信息串成一个trace结构每个任务一个唯一trace_id每一轮循环一个span父子关系能直接反映调用链。之前在几个项目里排查问题效率提升非常明显遇到一个响应异常直接看trace里卡在哪一步是模型推理慢还是工具返回慢一目了然。不需要一开始就上复杂的观测系统先把trace_id和关键日志打出来后面要接APM或者自己搭看板都方便。4. 架构选型对比常见模式与实战取舍4.1 三种主流Agent架构模式怎么选我总结下来主流的Agent架构模式可以分成三类ReAct风格、Plan-and-Execute风格、以及分层多智能体风格。ReAct风格是最基础的模型在“思考-行动-观察”之间循环每一步都根据已有信息决定下一步做什么。它的优点是实现简单、灵活性强适合任务目标不算特别明确、需要边走边看的场景。缺点是每一步都依赖模型的实时判断步骤一多累计推理成本和延迟会明显上去。Plan-and-Execute风格是把任务拆成两段先让模型做规划生成一个逐步计划然后由一个执行器按计划执行执行过程中再根据实际情况调整。这种模式适合目标明确、步骤清晰的批处理任务比如“每天定时整理昨天的数据并生成报告”。它的优点是减少反复推理缺点是计划不准确时整个任务会走偏纠错成本高。分层多智能体风格是有一个主控Agent负责任务拆解和调度多个子Agent各司其职比如一个负责查询、一个负责分析、一个负责输出。它适合子任务边界特别清晰、需要不同系统能力组合的场景。但代价是架构复杂度最高上下文在多Agent之间传递很容易丢失成本也成倍增长。选型时我的判断标准是先看任务是否需要长期规划。如果用户问题通常能在一两次工具调用内解决就用ReAct如果用户问题经常是“做一下某某数据报告”这类有明确流程的长任务用Plan-and-Execute更稳只有当子任务的专业性差异极大、而且边界非常清晰时才考虑多智能体。4.2 单Agent还是多Agent先别急着组“团队”多Agent听起来很酷但至少在我接触过的项目里大多数场景用单Agent加工具组合就能解决。很多团队一上来就设计三个Agent一个负责规划、一个负责执行、一个负责总结结果发现Agent之间的信息同步成了灾难每个Agent都要重新解释一遍上下文算下来成本是单Agent的三倍效果还不一定更好。我建议先做个简单的判断任务是否能被拆成有明确输入输出边界的子任务各子任务之间是否需要频繁共享中间状态。如果答案是“边界不清晰”或“频繁共享”那就老老实实用单Agent。多Agent真正的价值场景是那种不同领域专家协作的场景比如财务分析需要同时调用财税知识库、风险规则引擎和报表生成工具每个子任务都有独立且明确的上下文。除此之外我的经验是让一个Agent扮演多角色比拆成多个Agent更省心只要在system prompt里把不同阶段要用的角色定义清楚配合工具切换大多数业务场景足以覆盖。4.3 框架选型与自研轻量Runtime的平衡关于用框架还是自研我见过两种极端一种是什么都用框架最后被框架的抽象绑架另一种是什么都自己写连基础HTTP调用都要手撸项目进度慢得没法看。我的建议是分阶段走。快速验证阶段直接用主流Agent编排框架把工具定义好、流程跑通重点验证业务逻辑和模型效果。这个阶段的核心目标是搞清楚“这个场景到底可不可行”不值得在底层代码上花太多时间。进入生产阶段以后就要开始认真评估框架给你带来了什么、又限制了什么。如果框架的核心循环完全满足你的需求那就继续用只是要确保团队里有人能深入源码级排查。如果发现每次定制都很别扭就要考虑把核心循环收回来自己控制。我自己最后的做法是保留框架的部分工具链但核心循环和状态机完全自研这样的组合既保证可控性又不用重造所有轮子。5. 实操经验实现一个轻量Harness Runtime的完整路径5.1 最小运行时的骨架设计我先给大家一个可以直接参考的轻量Runtime骨架。我用Python写异步风格因为Agent场景天然IO密集异步能有效提升吞吐。核心类就四个LLM客户端、工具注册表、记忆管理器、AgentRuntime。class ToolRegistry: def __init__(self): self._tools {} def register(self, name, description, parameters, func): self._tools[name] { name: name, description: description, parameters: parameters, func: func, } def schemas(self): return [ { type: function, function: { name: t[name], description: t[description], parameters: t[parameters], }, } for t in self._tools.values() ] def execute(self, name, arguments): if name not in self._tools: return {status: error, message: ftool {name} not found} try: result self._tools[name][func](**arguments) return {status: success, result: result} except Exception as e: return {status: error, message: str(e)}这个注册表就是我前面说的统一收口工具集中管理、执行结果标准化、异常不直接抛出而是转成结构化信息。5.2 核心循环与终止控制接下来是AgentRuntime的核心循环。这里展示的是最关键的控制逻辑包括最大步数、工具调用记录和终止判断。class AgentRuntime: def __init__(self, llm, registry, memory, max_steps10, budget1.0): self.llm llm self.registry registry self.memory memory self.max_steps max_steps self.budget budget self.cost 0.0 async def run(self, user_request): messages [{role: user, content: user_request}] tool_usage_count {} for step in range(self.max_steps): response await self.llm.chat( messagesmessages, toolsself.registry.schemas(), ) self.cost response.cost if self.cost self.budget: return {status: budget_exceeded, data: messages} if response.tool_calls: for call in response.tool_calls: tool_name call.function.name arguments call.function.arguments tool_usage_count[tool_name] tool_usage_count.get(tool_name, 0) 1 if tool_usage_count[tool_name] 2: messages.append({ role: tool, tool_call_id: call.id, content: 该工具结果已获取请直接基于现有信息回答不要重复调用。, }) continue result self.registry.execute(tool_name, arguments) messages.append({ role: tool, tool_call_id: call.id, content: str(result), }) continue else: return {status: done, answer: response.content} return {status: max_steps_exceeded, data: messages}这段代码的核心价值是把终止控制和重复调用抑制直接做进循环里。实际项目中你还会加入trace、超时等但有了这个骨架后面所有扩展都有的放矢。5.3 错误处理与降级策略模型和工具都是非确定性组件错误处理不能靠运气。我经过多次踩坑后总结了一套降级策略。模型输出解析失败时不要立刻终止整个任务可以先尝试提取JSON子串、修复引号、再做一次解析。如果还不行就给模型回填一条“你的输出格式不符合要求请重新以合法JSON格式返回”有时一次重试就能解决问题。工具执行失败时好的做法不是把异常抛给用户而是把错误信息以tool message的形式返回给模型让模型基于错误信息重新决策。要让模型具备自我修正的机会。比如工具报“权限不足”模型可以直接告知用户没有权限而不是整个任务崩溃。连续多次失败时要果断终止并返回当前已收集到的最有价值信息。我设置过一个规则同一个工具连续失败三次就终止该分支给最终答案加上“部分信息获取失败”的前缀至少保证用户拿到了能用的部分结果。6. 常见问题与排查经验实录6.1 模型一直在循环调用同一个工具这是最高频的问题症状是Agent在一个查询动作上反复打转消耗大量token却没有进展。我排查了几个案例后发现大部分原因是工具返回的信息不足以支撑模型做下一步决策或者prompt里缺少明确的终止指导。解决方案有两步一是在工具返回结果时尽量包含一个summary字段直接给模型一个决策依据二是在system prompt里加入明确指令比如“当你已经获得足够信息回答用户问题时必须直接给出最终答案不得再调用任何工具”。同时代码层面像我前面展示的加一个连续调用次数限制做兜底。6.2 工具参数JSON解析失败模型偶尔会把arguments输出成非法JSON或者混入多余文本。症状很直接解析器报错任务中断。遇到这个问题先别骂模型因为工具参数描述越复杂模型出错概率越高。我的排查习惯是把原始输出记录下来看看是前后多了Markdown代码块标记还是引号/括号不规范。处理上解析器要做两层容错先提取最外层的JSON块再尝试解析解析失败时可以尝试去掉换行、修复单双引号或者走一次模型自修复。最彻底的办法是模型侧尽量使用结构化输出能力强制模型按JSON Schema输出。6.3 上下文被工具返回结果撑爆工具返回一个几千行的数据表模型还没开始分析上下文窗口就快满了。这个问题在查询类工具中特别常见。根治办法是工具侧先做字段裁剪只返回任务真正需要的字段比如查询订单就只返回订单号、状态、金额不要返回所有日志。再进一步可以在工具执行后加一个摘要层当返回内容超过一定字符数时先用一个轻量模型生成摘要再把摘要回填给主Agent。我在一个数据分析项目里就这么做上下文占用直接降到原来的五分之一。6.4 常见问题速查表症状可能原因排查方向解决建议Agent反复调用同一工具工具返回信息不足、缺少终止指令查看工具返回内容是否充分、prompt终止条件是否明确增加summary字段、加连续调用次数限制参数JSON解析失败模型输出非法JSON、参数描述复杂记录原始输出检查格式偏离情况多层解析、结构化输出、模型自修复上下文窗口溢出工具返回内容过多、历史消息堆积统计工具返回字节数、上下文峰值为多少字段裁剪、摘要层、滑动窗口任务突然中断无结果工具异常抛出、模型输出无法解析查看服务端日志里的异常堆栈和trace统一ToolResult、错误回填模型成本飙升无预算控制、循环无步数上限查看每次模型调用token数和总步数加预算检查、最大步数、重复调用抑制6.5 一个排查思路的完整示例某次线上Agent一直没法回答电商订单问题用户反馈总是“系统繁忙”。我一开始看应用日志只看到Agent抛了个KeyError完全不知道哪一步出问题。后来打开接的Trace看到一个工具调用的span耗时23秒紧接着就报错了。打开工具参数一看订单号传成了空字符串而查询工具本身对空参数没做校验直接抛异常。这个问题的根子其实在参数转换和工具校验两层一是工具注册表没有对必填参数做前置校验二是查询工具没有对空字符串做防御。修好这两处再把错误信息回填机制加上类似问题就再也没有出现过。这类问题靠模型本身是发现不了的只有把可观测性做好、把状态流转捋清楚才能快速定位到具体环节。最后再分享一点个人经验在这些Agent项目里摸爬滚打之后我有两个很深的体会。第一个是一开始不要想着上多复杂的架构先把最核心的执行循环写出来跑通几个真实任务再一点点加记忆、限流、观测、多Agent。每一步只改一个变量出了问题能立刻定位。第二个是Agent运行时的价值往往在系统出问题时才能体现出来。真正让一套Agent架构稳定扛住生产的不是模型有多聪明而是围绕循环的控制和兜底做得够不够扎实所以值得把精力花在基础设施上别只盯着炫酷的效果。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。