☰
从LLM调用到Harness Runtime:AI Agent架构选型演进路线
2026/10/19 5:27:52 网站建设 项目流程

做 AI Agent 架构选型这段时间,我有个特别直观的感受:很多人不是被技术难倒的,是被选项的数量难倒的。从一个单次 LLM 调用——把 Prompt 发给模型,拿回文本结果——到一个完整的 Harness Runtime——调度、记忆、工具治理、可观测性、运行追踪全都齐备——中间隔着好几个数量级的复杂度。恰恰是这段跨度,成了大家在设计 Agent 系统时最容易走偏的地方。

这篇文章想用一条“架构演进线”把这跨度讲透。我会把四个递进方案的具体形态摆出来,再解释每个方案适合什么任务、边界在哪里,顺带把我在实际选型中踩过的坑和判断逻辑一并整理出来。不管你是刚开始写 Agent 原型,还是在为线上服务规划运行时,这套思路应该都能让你少走点弯路。

1. 先看清楚:Agent 系统的复杂度藏在哪些维度

1.1 复杂度不是线性增长的

很多人以为 Agent 架构就是“在 LLM 外面多包几层代码”,这个理解会吃大亏。从一次单点调用到完整运行时,复杂度是跳跃式增长的。你多引入一个组件,就要多处理一份状态、多一个故障点、多一层调试成本。

我见过不少团队,一开始只打算做个“调用模型总结一下邮件”的小工具,结果在跑通之后很快就触到了边界:上下文塞满了、工具调用的结果不稳定、上一步结果和下一步动作对不上。这些都不是模型能力问题,而是架构问题。

把复杂度拆开看,其实就是四个维度:

  • 状态管理:一次调用没有状态,Agent 循环需要会话状态,完整的运行时需要可恢复的持久化状态。
  • 工具执行:单体函数调用和分布式工具执行,导致副作用与可靠性问题的量级完全不同。
  • 决策策略:是让模型一步步自由推理,还是按预定义工作流执行,直接决定了系统的可预测性。
  • 运行保障:日志、监控、审计、回滚、人工介入,这些都只在跑到一定规模后才成为刚需。

这四个维度基本就是“从调用到 Runtime”演进的主轴。后续每一层架构,本质上都是在回答这四个维度各自要做到多完备的问题。

1.2 先决定复杂度方向,再选技术方案

选架构有个反直觉的原则:不是越完备越好,而是越匹配任务边界越好。一个只做文本分类的服务,强行套上完整 Agent 编排,只会白白增加延迟和成本。

我自己的经验是先用一张表把任务的“需求边界”画清楚。要处理的任务是单步的还是多步协作的?状态是纯对话式的还是必须在多个服务间共享的?允许失败重试还是必须一次成功?调试问题时你希望看到什么粒度的信息?

把这些写下来之后,架构选项会自然缩减到一两个。下面四个层级的方案,就是按照这种思路逐一展开的。

2. 最小可行方案:裸 LLM 调用跑通单点任务

2.1 架构形态与代码结构

最底层的方案不复杂,实际上就是直接调用模型服务,把输入的 Prompt 变成输出结果。在这个级别,你的业务代码里只有三个部分:请求组装、网络调用、结果解析。

import json from llm_client import ChatClient client = ChatClient(endpoint="your-llm-service/deploy-model-1") def summarize(text: str) -> dict: # 1. 组装请求 messages = [ {"role": "system", "content": "你是一个严谨的摘要助手。输出 JSON。"), {"role": "user", "content": f"请把下面的内容压缩成 200 字以内的摘要:\n{text}"}, ] # 2. 调用模型 resp = client.chat( messages=messages, temperature=0.3, max_tokens=512, timeout=10, ) # 3. 解析结果 return json.loads(resp["content"])

这段结构看起来简单,但真要放上生产,有几个参数和细节值得抠一抠。首先是temperature。摘要、分类这类判定型任务,我习惯压低到 0.1 到 0.3 之间,换来的稳定性非常明显。生成型任务比如写文案,才放开到 0.7 以上。

其次是max_tokens。很多人忽略这它,结果模型输出到一半被截断,还需要额外写截断处理。我通常会在 prompt 里明确限定输出格式(比如“输出 JSON”),并把max_tokens设成正常输出的 1.5 倍左右,留出缓冲。

最后是timeout。模型服务的延迟受负载影响很大,接口超时设置在 5 到 15 秒之间比较常见。但更重要的是要有重试机制。这里要小心:超时之后直接重试,在大流量下会变成对上游服务的“重试风暴”。正确做法是加指数退避,比如第一次等待 1 秒,第二次 2 秒,第三次 4 秒,同时设重试上限为 3 次。

2.2 这个方案能解决什么问题

裸 LLM 调用的定位很清楚:单步、无状态、判定或生成类任务。

我实际用这个级别做过的事情包括:工单自动分类、评论的情感打分、会议纪要的粗提要和关键词提取。这些任务的共同特点是没有多步推理,没有工具交互,一次调用拿结果就够了。

它的边界也很明确。一旦任务需要“先查一下知识库再回答”,或者“上一步结果决定下一步调用哪个接口”,纯调用方案就撑不住了。模型不知道外部世界,也不知道你之前问了什么,每次调用都是一次孤立的“失忆”对话。

2.3 单点调用阶段的典型坑

这个阶段最常见的坑是把业务逻辑写进了 Prompt。一开始你会嫌麻烦,把判断逻辑都塞在 system prompt 里,比如“如果输入包含退货关键词,就返回 1;包含物流关键词,就返回 2”。等到规则多了,模型开始各种误判,你才意识到 Prompt 里的确定性远远不够。

我的建议是:凡是能用代码写的分支,就别交给模型。模型只负责理解和生成,决策逻辑交给外层代码。这样排错时你能清楚地区分“模型理解错了”和“代码分支错了”。

另一个坑是忽视了响应格式的校验。模型返回的内容经常带多余文字,比如“好的,这是结果:{...}”。我习惯在解析 JSON 时先做一个预处理:找到第一个{和最后一个},截取中间内容再解析。这种小细节,实测能省掉大量因为格式错误而引发的报警。

3. 第一级晋级:给模型装上工具,Function Calling 架构

3.1 工具调用到底改变了什么

当任务从“读一段文字出结果”变成“需要查数据、调接口、执行动作后再回答”时,你就要面对一个关键机制:Function Calling(函数/工具调用)。它让模型不再只是一张“嘴”,而是有了一双可以操作外部系统的“手”。

它的核心流程是:你把可用工具的描述发给模型,模型不直接执行,而是返回“该调用哪个工具、参数是什么”的结构化结果,由你的代码实际执行工具,再把结果返回给模型继续推理。

def query_order_status(order_id: str) -> str: # 实际查询订单系统,返回状态文本 return "订单 20240305 已发货,预计 3 月 7 日送达" tools = [ { "type": "function", "function": { "name": "query_order_status", "description": "查询订单的物流状态", "parameters": { "type": "object", "properties": { "order_id": {"type": "string", "description": "订单编号"} }, "required": ["order_id"], }, }, } ] # 把 tools 传给模型 resp = client.chat(messages=messages, tools=tools, tool_choice="auto")

模型返回的内容可能带tool_calls字段。你的代码拿到之后,要判断它选了哪个工具、参数是什么,然后执行并把结果追加到对话里。

我再强调一个容易被忽略的点:追加到对话里的工具结果,本身也是对话消息的一种类型。它要带role信息,通常是“tool”或者“function”,具体以你用的服务协议为准。很多人忘记这一步,结果模型下一轮推理完全丢失了工具结果,整个循环就废了。

3.2 工具定义的质量决定了 Agent 的质量

工具定义的description不是给人看的,是给模型看的。模型决定选中哪个工具,依赖的是描述文本的准确性。描述写得含糊,模型就会乱选。

我在实践中的几个细节:

  • 工具名称用动词开头。比如get_weather、send_email,比weather、email_message更容易被模型正确调用。
  • 参数描述写清楚单位、格式、边界。数值参数写“单位是摄氏度”,日期参数写“格式为 YYYY-MM-DD”。这些都能显著降低模型传参的错误率。
  • 工具数量控制在合理范围内。一次请求传二三十个工具定义,模型在选型和传参上的错误率会明显上升。如果工具太多,可以考虑先做一层“工具路由”,用一个主工具去分发子任务。

3.3 工具执行层的可靠性设计

工具调用引入了新的故障模式:模型选错工具、参数格式错误、工具执行超时、工具返回异常数据。

我现在的做法是给工具执行加一层保护壳:

def safe_execute(tool_name: str, arguments: dict) -> str: try: result = REGISTRY[tool_name](**arguments) return json.dumps(result, ensure_ascii=False) except KeyError: return "错误:未找到该工具" except TypeError as e: return f"错误:参数不匹配,详情:{e}" except Exception as e: return f"错误:工具执行异常:{e}"

这层保护壳把“工具执行失败”这件事变成了一条普通文本消息,回递给模型去修正。模型看到错误信息后,有较大概率会自行调整参数重试。这个小设计本质上就是在还没有完整 Harness 的阶段,用最小成本提升了整个系统的容错能力。

4. 关键转折:从单轮调用到 Agent 循环

4.1 ReAct 循环的落地实现

加上了工具,模型依然只能走“单轮推理”。但真正的 Agent 需要的是“交替进行”的循环:观察外部反馈、思考下一步动作、执行动作、再观察。这就是 ReAct 模式的简版,也几乎是所有 Agent 运行时的核心引擎。

一个最基本的 Agent Loop 长这样:

def run_agent(goal: str, max_steps: int = 8) -> str: messages = [{"role": "system", "content": "你是一个能调用工具完成任务的助手。")] {"role": "user", "content": goal}] for step in range(max_steps): resp = client.chat(messages=messages, tools=tools) if hasattr(resp, "tool_calls") and resp.tool_calls: messages.append(resp.to_message()) for call in resp.tool_calls: result = safe_execute(call.name, call.arguments) messages.append({"role": "tool", "tool_call_id": call.id, "content": result}) else: return resp.content return "达到最大循环次数,任务未完成"

注意我限制了max_steps。这一步非常必要,否则模型可能在错误的路径上无限打转,浪费 token 不说,还会把延迟拖到不可接受的程度。任务越复杂,max_steps越大,但我不建议超过 8 到 10。超过这个数目,大概率不是任务复杂,而是循环设计出了问题。

4.2 记忆管理:Agent 循环的隐形瓶颈

Agent 循环一旦跑起来,对话消息数量会快速膨胀:用户输入、系统回复、工具返回值、下一轮模型回复……每一轮都要把历史消息全部发给模型。很快你就会被上下文窗口的容量卡住。

我在这个阶段会把记忆分成三层来处理:

  • 短期记忆:本次任务循环内的原始消息流,直接放在上下文里。
  • 工作记忆:对前面步骤的压缩摘要。每几轮循环之后,如果消息量超过预设阈值,就调用模型把已有历史概括成几百字的摘要,替代原始历史。
  • 长期记忆:超出当前任务的知识沉淀,比如用户偏好、历史结论、跨会话信息。这类内容通常落到向量数据库里,通过语义检索把相关内容拉回上下文。

这三层里,最容易忽略的是“工作记忆”。很多人在原型阶段直接堆消息,等上下文超限了才开始急。其实处理方式不复杂:在循环内部加一个检查点,累计消息数超过比如 10 条,就触发一次摘要压缩。

if estimate_tokens(messages) > 6000: summary = client.chat( messages=[{"role": "system", "content": "压缩对话历史。"}, {"role": "user", "content": json.dumps(messages)}] )["content"] messages = [{"role": "system", "content": f"已压缩的历史摘要:{summary}"}]

这样做的好处是让循环维持在恒定延迟上,不会随着任务推进越来越慢。坏处是丢了原始细节,所以摘要质量很关键。我自己会要求摘要保留三样东西:已完成的步骤、关键结果、剩余待办。

4.3 循环阶段的稳定性风险

到了 Agent 循环,模型的不可预测性会被放大。你面对的不再是一个单点输出,而是一条完整的决策链。任何一个环节的偏差,都会顺着循环传导并放大。

一个非常典型的场景:模型在第二步调用了查询接口,返回结果是“无记录”。模型基于这个结果,依然会煞有介事地编一个“查到了”的回复。这不是模型坏,是它在没有明确证据时倾向于编造连贯性内容。

我的对策是在工具结果里加一个明确的判定字段,比如“record_found: false”。模型看到结构化否定信息后,编造的概率会下降不少。更进一步的做法是,在 System Prompt 里明确写入“如果没有相关信息,直接告诉用户未找到,不要推测”。这种声明式的约束,比单纯指望模型自觉有效得多。

5. Agent 系统的工业级宿主:完整 Harness Runtime

5.1 Harness Runtime 与朴素循环的本质区别

现在,终于要说到标题里那个 Harness Runtime 了。有人把它理解成“加强版 Agent Loop”,我觉得格局小了。Harness Runtime 的本质,是把 Agent 从一个“跑在脚本里的循环”升级成一个“跑在平台上的服务”。

两者的差别,可以类比“自己在家做饭”和“开一间规范化餐厅”的区别。做饭只看结果,开餐厅要看供应链、卫生标准、厨师排班、顾客投诉处理、成本核算。Agent 跑在 Harness 上,承担的不只是任务执行,还包括资源管理、风险控制、审计追踪和生命周期维护。

一个完整的 Harness Runtime 至少包含五层:编排层、执行层、状态层、治理层、可观测层。这五层缺一不可。

5.2 编排层:从“自由跑”到“有约束地跑”

朴素 Agent 循环是让模型完全自由地一步步推理。这在原型阶段没问题,生产环境就不行了。生产需要的是可控:哪些工具能调用、哪些数据能访问、哪些流程必须经过人工确认。

Harness 的编排层通常支持两种模式,混用也很常见:

  • 流程编排模式:预先定好执行流程的 DAG(节点 A 完成后走节点 B 或节点 C),每个节点可以是一个 LLM 调用、一段代码或一个人工审批。
  • 动态规划模式:任务本身不确定,由模型实时拆解子目标,但每个子动作必须经过工具白名单校验。

用 YAML 定义工作流是我偏好的方式:

workflow: id: "customer_refund" nodes: - id: "talk_to_user" type: "llm_call" prompt: "收集用户的退款原因和订单编号" - id: "check_order" type: "tool_call" tool: "query_order" depends_on: "talk_to_user" allowed_args: ["order_id"] - id: "human_approve" type: "manual_review" depends_on: "check_order" policy: "refund_over_500" - id: "execute_refund" type: "tool_call" tool: "refund_api" depends_on: "human_approve"

这个配置文件的语义很清晰:每一步做什么、依赖什么、什么条件允许执行。你可以把它交给一个简易的 DAG 调度器去推进。好处是,当某个节点挂了,你能立刻定位到是哪个环节、哪类任务出了问题,而不是在冗长的循环日志里大海捞针。

5.3 执行层与状态持久化:让 Agent 可以中断和恢复

线上 Agent 有一个朴素循环很难处理的场景:任务执行到一半,服务重启了。如果状态全在内存里,重启等于任务直接丢了。用户拿着已付款订单来询问进度,结果 agent 一脸茫然,这种体验是灾难级别的。

所以 Harness 必须做状态持久化。我通常的做法是:

  • 每个 Agent 任务分配一个全局唯一的run_id。
  • 每个步骤(LLM 调用、工具调用、人工审批)的状态都写进存储,比如记录为pending、running、succeeded、failed、waiting_human。
  • 服务重启后,调度器读取未完成任务,从最后一个已完成步骤继续推进。
class StepRecord(BaseModel): run_id: str step_id: str status: str input: dict output: dict error: str | None = None created_at: datetime

这一步技术难度不高,但对架构观的影响很大。做完状态持久化之后,你会发现自己被迫把“步骤”“输入”“输出”“错误”这些概念显式化,而这正是从循环走向运行时最关键的思想转变。现在,每个步骤是独立可观测、可回放、可审计的实体。

5.4 治理与安全:工具权限、人工审核和护栏

完整的 Harness 必须有能力说“不”。模型可以规划,但不代表它有权直接执行任何动作。特别是涉及资金、隐私、删除类操作时,必须引入权限控制和人工审批闸门。

我的治理框架分三层:

  • 工具白名单:按 run 类型配置可用工具集,比如客服 Agent 只能访问订单查询、政策查询;营销 Agent 只能访问内容生成和渠道发布。
  • 敏感操作拦截:工具名称或参数命中敏感规则(如send_money、delete),自动转人工审核,模型无法直接执行。
  • 输出护栏:模型回复给用户之前,过一次规则检查。检查关键词、敏感信息泄露、格式规范,不合格则重新生成或转人工。

5.5 可观测性:Agent 调度的黑盒困境

可观测性是在 Harness 里最容易显得“做得差不多”但实际差很多的部分。普通的日志只能记录“调了哪个模型、返回了什么”,而 Agent 需要的是“为什么走了这条路径”。

路径归因这件事,我踩过很深的坑。原来上线发行时上一版失败,但是日志只显示这一版出了错。现在每次 Agent 完成一轮任务,我会把整条决策链记录下来:步骤顺序、每一步的模型输出、工具调用与返回值、延迟消耗、重试次数、最终结论。

trace_entry = { "run_id": run_id, "step_index": step_index, "type": "tool_call", "tool_name": "query_order", "arguments": {"order_id": "20240305"}, "result_preview": "订单已发货", "latency_ms": 320, "token_usage": 510, "model": "deploy-model-1", }

这些数据不只为事后排错,更是 Agent 优化的基础。当你要判断“该换模型”“该精简 Prompt”“该减少工具数量”时,没有路径级别的追踪数据,你只能靠感觉决策。有了数据,你就能明确地看到,80% 的失败其实集中在某一个工具调用的参数解析上。

6. 架构选型的决策框架

6.1 一张表看清四个方案的边界

把前面几个方案放在一起对比,你会看到清晰的层级边界。我总结了最关键的四列维度,供你对照判断。

维度裸 LLM 调用工具调用Agent 循环Harness Runtime
任务类型单点生成/分类单轮推理+外部数据多步推理、动态决策复杂业务流、多 Agent 协作
状态管理无状态无状态或轻量上下文会话内状态可恢复的持久化状态
容错能力重试工具异常兜底循环内多步回退任务级恢复与人工介入
可观测性单次日志工具调用日志步骤链日志全链路追踪与审计
最小成本极低低中较高(需平台投入)

如果你做的任务是“判断这个用户评论是正面还是负面”,用第一层就够了。如果任务是“根据用户提问查知识库并给出依据充分的回答”,第二层。如果任务是“用户描述需求后,自主拆解为多个步骤并逐步完成”,第三层。如果任务是“这个 Agent 要服务全公司几百个业务方,需要审计、权限和多系统交互”,那么直接考虑第四层。

6.2 我的演进路线:从最小闭环开始

多数时候你不是一开始就知道要落在哪一层的。我也一样。我现在的建议是,不管最终目标多宏大,先用第一层方案把最小闭环跑通。这样做有三个好处:第一,最低成本验证任务的价值;第二,暴露出真实任务中的状态和工具需求;第三,为后面的演进提供对照基线。

跑通之后,按下面的顺序递增:

  1. 如果模型需要查询数据,引入工具调用。
  2. 如果任务需要多步骤决策,加上循环与记忆管理。
  3. 当任务规模变大、失败影响上升,再补齐 Harness 的编排、状态持久化和可观测性。

每加一层,都要问自己:当前遇到的真实痛点是什么?是不是这层能解决的?如果只是“感觉应该有个调度器”,那你很可能只是在给原型镀金。

6.3 团队与成本的现实考量

团队规模对架构选择的影响,往往比技术本身更大。三个人以内的团队,我强烈不建议一开始就铺 Harness。先把核心链路做对,把工具调用的可靠性打磨好,比提前搭一个平台更重要。等到团队到了五六个人,维护 Harness 的成本才有意义摊薄。

成本也要算一笔账。Agent 循环和 Harness Runtime 的 token 消耗是远高于单次调用的。一次复杂 Agent 任务,可能消耗几万 token。如果这部分任务每天几千次,成本就会成为不可忽视的因素。我遇到过团队把成本估算漏掉,结果系统上线一周就被账单吓了一跳的事。

降低 token 成本有几个实际办法:压缩历史摘要、限制工具返回长度、优先用更大的模型做复杂节点而用小模型做简单任务、对不重要的中间输出降低max_tokens。这些都是可以在架构内优化的点,但前提是你得有 token 计量的数据。还是那句话,可观测性是一切优化的起点。

7. 实操中踩过的坑与排查技巧

7.1 坑一:上下文无限膨胀

这个坑几乎所有人都会踩。Agent 循环执行得越久,上下文越长,latency 和成本同步上升。前期我做过一次工具调用的案例,请求参数里塞了 20 条历史记录,每轮请求显式多出 3000 tokens,光这个就拖慢了 1.5 秒。

排查方式是看 token 用量曲线。如果发现消息量呈线性增长,就需要加工作记忆压缩。处理方式前面已经讲了,不再重复。这里只强调一个态度:宁可多压缩一点丢失一些边缘细节,也不要让上下文总量失控。

7.2 坑二:工具调用解析失败

工具调用阶段,模型返回的tool_calls本身有时不合法。参数可能多一个字段、少一个字段,或者是个奇怪的字符串。直接扔给执行层会直接抛异常。我的惯例是加解析前校验,解析失败则不要立即重试整个模型调用,而是把错误消息拼回对话,让模型重新生成。

这个“让模型看见自己的错误再修正”的模式,实测比简单重试的效果好很多。因为模型看过错误信息之后,往往能自己补全参数或换工具。这一点在跳过工具层直接上 Harness 的团队里特别容易漏掉,他们习惯用传统的异常重试逻辑,但 LLM 的失败模式跟传统程序完全不同。

7.3 坑三:状态不同步导致的多实例问题

一旦 Agent 同时处理多个任务,或者同一个任务在不同节点之间流转,状态一致性就成了问题。我碰到过一次任务被两个执行实例重复执行,导致同一个动作被执行了两次。事后追查,发现是没有给任务加分布式锁。

解决方式不复杂:状态更新使用条件更新,比如“只有当状态为pending时才能更新为running”,失败则说明还有其他实例在处理。这类问题在 Harness 架构里尤其要留意。加一层乐观锁或数据库条件更新,成本很低,但能避免很多灾难性后果。

7.4 坑四:测试与评估体系跟不上

在 Agent 循环和 Harness 层,简单单元测试远远不够。你面对的是“同样的任务,不同输入会产生不同路径”的系统。包一层穷举测试没有意义,你需要的是评估集:收集一组真实且有代表性的任务,把 Agent 跑出来的结果和人工标注答案比对,统计成功率和满意度。

我把这项工作比喻成给 Agent 建立“体检报告”。每改一次 Prompt、每调一次模型、每换一次工具参数,都拿这组任务跑一遍,看分数是升还是降。没有这个体检机制,你就是一边改一边退化,却浑然不觉。

可在实践上,第一批评估集不用太大,五六十个典型任务就能发现问题。随着线上数据积累,再扩充规模。这个模块的建设难度不高,但它决定了你后续能不能安全迭代。

7.5 一个小技巧:结构化失败分类

排错时我习惯把失败问题分成三类:模型理解错误、工具执行错误、流程编排错误。模型理解错误的典型特征是模型选错了工具或者理解错了意图;工具执行错误通常是参数不匹配或接口超时;流程编排错误则表现为步骤没有按预期顺序执行、依赖条件未生效。

这个分类对定位非常有用。别在出现“任务失败”时急着去翻那几万字的运行日志,先确定它属于哪一类,再按类别去查对应的环节,定位效率会高很多。这也是我做可观测性设计时的基本框架:不是记录所有日志,而是记录能区分这三类问题的关键字段。

选型这件事,我的个人体会是:不要被“进阶”的诱惑带走,也不要被“平台化”的宏大叙事裹挟。先把任务边界画清楚,再决定从哪一层起步。哪怕最终你要的是完整 Harness Runtime,也建议从最小闭环开始演进,每一步都有明确的痛点驱动,每一步都建立在实测数据之上。这样搭出来的系统,才是真正能长久运行的 Agent 架构。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询