1. 从两个开源项目看 AI Agent 的架构分野第一次看到 OpenClaw 和 OpenCode 这两个名字放在一起,我的直觉是:这俩大概率是同一类东西的两种实现路径。实际拆下来发现,它们确实都在做"让 AI 模型自主调用工具完成任务"这件事,但架构取向几乎是两个极端。一个偏向"重编排、强约束",另一个偏向"轻内核、重扩展"。这种差异不是拍脑袋决定的,而是各自对"AI Agent 到底该长什么样"这个问题的不同回答。先把结论摆前面:OpenClaw 更像一个"任务编排框架",OpenCode 更像一个"能力运行时"。前者关心的是"一个复杂任务怎么被拆解、调度、回收",后者关心的是"一个模型怎么以最低成本接入各种工具并保持上下文连贯"。理解这条主线,后面所有的模块设计、通信机制、状态管理都能对上号。这篇文章适合三类人看:正在选型 Agent 框架的工程师、想自己撸一个 Agent 内核的开发者、以及被各种"Agent 架构图"绕晕的产品同学。我会尽量把每个设计决策背后的"为什么"讲清楚,而不是只罗列模块名。涉及具体参数和代码的地方,我会给出可直接参考的写法,但你要知道这些是基于常见实践的合理补全,不是官方文档的逐字搬运。2. 核心架构思路的拆解与选型逻辑2.1 两种架构哲学:编排优先 vs 运行时优先OpenClaw 的核心假设是:Agent 的复杂度主要来自任务本身。一个真实场景里的任务,比如"帮我分析这份销售数据并生成周报",它不是一次模型调用能搞定的,需要拆成读文件、算指标、写文案、格式化输出等多个步骤,步骤之间还有依赖关系。所以 OpenClaw 把大量精力花在任务图(Task Graph)的构建和调度上,模型只是图中的一个"决策节点"。OpenCode 的核心假设不一样:Agent 的复杂度主要来自工具生态的多样性。今天要调数据库,明天要调浏览器,后天要调某个内部 API,如果每接一个工具都要改框架,那这个框架就没法用。所以 OpenCode 把精力花在工具注册、上下文注入、流式响应这些"运行时能力"上,任务编排反而交给模型自己用自然语言去规划。这两种假设没有对错,只有适配场景的差异。我个人的经验是:流程相对固定、步骤可枚举的任务,用编排优先的架构更稳;流程高度动态、工具频繁变化的任务,用运行时优先的架构更灵活。下面这张表是我在实际选型时会对照的维度:对比维度OpenClaw(编排优先)OpenCode(运行时优先)任务定义方式显式任务图,节点可枚举隐式,模型自主规划工具接入成本需注册到编排层,改动较大插件式注册,改动小状态管理框架统一管理,可持久化依赖会话上下文,较轻调试难度图结构清晰,易定位依赖日志,链路较长适合场景固定流程、多步骤、需审计动态工具、快速迭代2.2 为什么会有这种分野:从"谁来做决策"说起要理解这两个项目的架构差异,得先回答一个更底层的问题:在一个 Agent 系统里,决策权到底在框架手里还是在模型手里?OpenClaw 的选择是"框架主导、模型辅助"。框架先根据任务类型生成一个任务图,每个节点是一个原子操作,模型只在需要"判断"的节点上被调用,比如"这个文件是 CSV 还是 Excel"、"这段文案的语气该正式还是轻松"。这样做的好处是可控性强——每一步做什么、做到哪了、失败了怎么回滚,框架都心里有数。坏处是灵活性差——如果任务超出了预设的图结构,框架就懵了。OpenCode 的选择是"模型主导、框架兜底"。框架只提供工具和上下文,具体调哪个工具、按什么顺序调,全由模型在对话中决定。框架的职责是保证工具调用不出错、上下文不丢失、流式输出不卡顿。好处是适应性强——模型能处理框架设计者没想到的任务组合。坏处是不确定性高——同样的输入,模型可能走出完全不同的路径,调试和复现都更麻烦。我踩过的一个坑是:早期用运行时优先的架构做一个"数据清洗"任务,模型有时候会跳过去重步骤直接输出,导致结果不稳定。后来改成编排优先,把去重做成强制节点,问题才解决。但反过来,做"客服问答"这种高度依赖上下文的任务时,编排优先的架构又显得太笨重,每次都要走一遍固定流程,响应慢且不自然。所以选型的第一原则是看任务的确定性程度,而不是看哪个框架"更先进"。2.3 架构分层:两者都绕不开的三层结构虽然取向不同,但 OpenClaw 和 OpenCode 在分层上其实有共识,都逃不开这三层:接入层:负责接收用户输入、管理会话、处理鉴权。这一层两者差异不大,都是标准的 Web 服务那一套。决策层:这是分歧最大的地方。OpenClaw 在这一层放了任务图调度器,OpenCode 在这一层放了模型对话管理器。执行层:负责实际调用工具、执行代码、访问外部资源。两者都采用"工具注册 + 统一调用接口"的模式,但 OpenClaw 的工具是绑定到任务节点的,OpenCode 的工具是全局可用的。这个三层结构不是谁规定的,而是任何 Agent 系统都必然要处理的三类问题:怎么进来、怎么决定、怎么执行。你在设计自己的 Agent 时,也可以先用这个框架去套,看每一层你打算放什么,放的东西是否符合你的任务特征。3. 核心模块的细节解析与实操要点3.1 任务调度模块:OpenClaw 的心脏OpenClaw 的任务调度模块是整个项目最值得细看的部分。它的核心是一个有向无环图(DAG)调度器,每个节点代表一个原子操作,边代表依赖关系。调度器的工作是:找到所有入度为 0 的节点,并行执行它们,执行完后更新下游节点的入度,直到所有节点执行完毕。这个设计的关键在于节点状态的精确管理。每个节点有五种状态:pending、running、success、failed、skipped。调度器在每一轮循环中做三件事:扫描所有pending且入度为 0 的节点,标记为running并提交执行。收集执行结果,成功的标记success,失败的标记failed。对于failed的节点,根据预设策略决定是重试、跳过还是终止整个图。这里有个容易忽略的细节:失败传播策略。如果一个节点失败了,它的下游节点应该怎么办?OpenClaw 默认是"下游全部标记skipped",但你可以配置成"下游继续执行,只是把失败信息作为输入传下去"。后者在"部分失败可容忍"的场景下很有用,比如批量处理 100 个文件,有 3 个失败了,你不想让剩下 97 个也停掉。实操中我建议你这样配置重试策略:retry_policy: max_attempts: 3 backoff: exponential base_delay_ms: 500 max_delay_ms: 8000 retry_on: [timeout, rate_limit, transient_error] abort_on: [auth_error, invalid_input]这个配置的意思是:最多重试 3 次,退避时间指数增长(500ms、1000ms、2000ms),只对超时、限流、临时错误重试,遇到鉴权失败或输入非法直接终止。为什么要区分retry_on和abort_on?因为有些错误重试一万次也没用,比如 API key 错了,你重试只是浪费时间和配额。这个区分能帮你省下大量无谓的等待。注意:DAG 调度器最怕的是"隐式依赖"。如果两个节点实际上有依赖关系,但你在建图时没连边,调度器就会并行执行它们,导致结果错乱。我的经验是:宁可多连边,不要少连边,串行执行慢一点,但结果是对的。3.2 工具注册与调用:OpenCode 的扩展点OpenCode 的工具注册机制是我见过比较优雅的设计。它用一个统一的Tool接口把所有能力抽象掉,每个工具只需要实现三个方法:class Tool: def name(self) -> str: """工具的唯一标识,模型通过这个名字调用""" pass def schema(self) -> dict: """参数的 JSON Schema,用于校验和生成模型可读的描述""" pass def execute(self, params: dict, context: dict) -> dict: """实际执行逻辑,context 里带着会话状态""" pass这个设计的精妙之处在于schema()方法。它不只是给框架做参数校验用的,还会被转换成模型能理解的工具描述,注入到对话上下文中。也就是说,你写一次 schema,模型就知道这个工具怎么用、参数是什么类型、哪些是必填的。这比手写工具描述文档靠谱多了,因为 schema 和实际执行逻辑是绑定的,不会出现"文档说参数是 string,实际要 int"这种破事。工具调用的完整链路是这样的:模型在对话中输出一个工具调用请求,格式是{"tool": "read_file", "params": {"path": "/data/a.csv"}}。框架拦截这个请求,先用schema()校验参数,不合法就直接返回错误给模型,让它重新生成。校验通过后,调用execute(),把结果序列化成 JSON。把结果作为一条tool_result消息追加到对话历史里,继续下一轮模型调用。这里有个实操要点:工具结果的体积控制。如果你调一个工具返回了 10MB 的数据,直接塞进上下文,模型要么超长截断,要么直接报错。OpenCode 的做法是给每个工具的结果设一个max_result_size,超过就截断并附上提示"结果已截断,共 X 字节,显示前 Y 字节"。我一般把这个值设在 8KB 到 32KB 之间,具体看模型上下文窗口大小。提示:工具命名尽量用"动词_名词"的格式,比如read_file、query_db、send_email。模型对这类命名的理解准确率明显高于fileReader或dbQuery这种驼峰式。这是我在多个项目里对比出来的经验,不是玄学。3.3 上下文管理:两者都头疼的问题上下文管理是 Agent 架构里最容易被低估、也最容易出问题的部分。OpenClaw 和 OpenCode 在这块的策略不同,但都绕不开三个核心问题:存什么、存多久、怎么取。OpenClaw 因为任务图是显式的,上下文管理相对简单:每个节点的输入输出都挂在节点上,节点执行完就把结果存到图的状态里,下游节点需要时直接从图里取。这种"图即上下文"的方式很清晰,但缺点是跨任务无法复用——上一个任务的中间结果,下一个任务拿不到。OpenCode 因为对话是连续的,上下文管理更接近传统的会话管理:维护一个消息列表,每条消息有角色(user/assistant/tool)和内容。随着对话轮次增加,消息列表会越来越长,最终超出模型上下文窗口。这时候就需要上下文压缩。我实测下来比较有效的压缩策略是"滑动窗口 + 摘要":保留最近 N 轮完整对话(N 一般取 6 到 10)。更早的对话,用模型生成一段摘要,替换掉原始消息。工具调用的结果,如果体积大且不再被引用,直接丢弃,只保留一句"曾调用 X 工具,结果已省略"。这个策略的关键参数是 N 和摘要长度。N 太小,模型会"失忆",忘记前面聊过什么;N 太大,压缩效果不明显。我的经验值是:对于 8K 上下文窗口的模型,N 取 6;对于 32K 的,N 取 10 到 15。摘要长度控制在 200 到 500 字之间,太短丢信息,太长又占空间。上下文策略适用场景优点缺点全量保留短对话、调试期信息完整很快超限滑动窗口长对话、生产环境实现简单早期信息丢失摘要压缩超长对话保留主线摘要本身有损图状态存储多步骤任务结构清晰跨任务不复用3.4 模型接入层:别小看这层的坑模型接入层看起来最简单——不就是调个 API 吗?但实际做下来,这层的坑一点不少。OpenClaw 和 OpenCode 都支持多模型接入,核心要处理的问题包括:流式响应、函数调用格式差异、错误重试、配额管理。流式响应是必须的,因为 Agent 的响应时间通常很长(要调工具、要等结果),如果不流式输出,用户会以为程序卡死了。但流式响应和函数调用有个冲突:函数调用的参数是流式返回的,你得等参数完整了才能执行。所以接入层要做一个缓冲,把流式的参数片段拼起来,拼完整了再触发工具调用。不同模型的函数调用格式差异也很大。有的用function_call字段,有的用tool_calls数组,有的把参数放在arguments里当字符串,有的直接给对象。接入层要做一个格式归一化,把各家格式统一成内部标准格式,上层逻辑就不用关心底层是哪个模型了。def normalize_tool_call(raw_call, provider): if provider == "provider_a": return { "name": raw_call["function"]["name"], "params": json.loads(raw_call["function"]["arguments"]) } elif provider == "provider_b": return { "name": raw_call["name"], "params": raw_call["input"] } # 其他 provider 的映射...这个归一化函数看着简单,但每接一个新模型就要加一个分支,维护成本不低。我的建议是:把映射关系写成配置,而不是硬编码在代码里,这样加新模型时改配置就行,不用改代码重新部署。注意:模型接入层一定要做超时和熔断。我遇到过某个模型服务商突然变慢,导致整个 Agent 卡住的情况。后来加了 30 秒超时和连续 5 次失败熔断,问题就没再出现过。熔断后要有一个恢复探测机制,比如每 60 秒试一次,成功了就恢复。4. 实操过程与核心环节实现4.1 从零搭一个最小可用的编排型 Agent光看架构不够,得动手。下面我用 OpenClaw 的思路,搭一个最小可用的编排型 Agent,任务是"读取一个 CSV 文件,计算某列的平均值,输出结果"。这个任务足够简单,但包含了编排型 Agent 的所有核心环节。第一步:定义任务图。这个任务可以拆成三个节点:read_csv:读取文件,输出原始数据。calc_avg:接收原始数据,计算指定列的平均值。format_output:接收平均值,格式化成自然语言。依赖关系是read_csv→calc_avg→format_output,一条链。第二步:实现节点执行器。每个节点是一个函数,接收上游的输出作为输入,返回自己的输出:def read_csv(inputs, context): path = context["params"]["file_path"] with open(path) as f: data = list(csv.DictReader(f)) return {"rows": data, "columns": list(data[0].keys())} def calc_avg(inputs, context): rows = inputs["read_csv"]["rows"] col = context["params"]["target_column"] values = [float(r[col]) for r in rows if r[col]] return {"average": sum(values) / len(values), "count": len(values)} def format_output(inputs, context): avg = inputs["calc_avg"]["average"] col = context["params"]["target_column"] return {"text": f"列 {col} 的平均值是 {avg:.2f}"}第三步:实现调度器。调度器负责按依赖顺序执行节点:def run_graph(graph, context): results = {} pending = set(graph.nodes.keys()) while pending: ready = [n for n in pending if all(d in results for d in graph.deps[n])] if not ready: raise RuntimeError("检测到循环依赖或死锁") for node in ready: inputs = {d: results[d] for d in graph.deps[node]} try: results[node] = graph.nodes[node](inputs, context) except Exception as e: results[node] = {"error": str(e)} pending.remove(node) return results这段代码不到 20 行,但已经是一个可用的 DAG 调度器了。关键点是ready的判断条件:所有依赖都在results里,说明上游都执行完了。如果某一轮ready为空但pending还有节点,说明有循环依赖,直接报错。第四步:接入模型。上面的节点都是纯函数,没有用到模型。如果要加一个"判断 CSV 的哪一列是数值列"的节点,就需要调模型:def detect_numeric_column(inputs, context): columns = inputs["read_csv"]["columns"] prompt = f"从这些列名中选出最可能是数值列的:{columns}" response = call_model(prompt) return {"column": response.strip()}这个节点插在read_csv和calc_avg之间,calc_avg的target_column就从context里取改成从inputs里取。这样整个流程就变成了"读文件 → 模型判断列 → 算平均 → 格式化",一个真正的 Agent 流程。4.2 从零搭一个最小可用的运行时型 Agent同样的任务,用 OpenCode 的思路来做,结构完全不同。运行时型 Agent 没有预设的任务图,只有一个对话循环和一组工具。第一步:定义工具。把读文件、算平均、格式化都做成工具:tools = [ { "name": "read_csv", "description": "读取 CSV 文件,返回列名和前几行数据", "params": {"path": "string, 文件路径"} }, { "name": "calc_average", "description": "计算指定列的平均值", "params": {"path": "string", "column": "string"} }, { "name": "finish", "description": "输出最终结果,结束任务", "params": {"text": "string, 最终答案"} } ]第二步:实现对话循环。核心逻辑是:把工具描述和用户问题一起发给模型,模型返回工具调用就执行,返回finish就结束:def run_agent(user_query, tools, max_turns=10): messages = [ {"role": "system", "content": build_tool_prompt(tools)}, {"role": "user", "content": user_query} ] for _ in range(max_turns): response = call_model(messages) if response.tool_call: tool_name = response.tool_call["name"] params = response.tool_call["params"] if tool_name == "finish": return params["text"] result = execute_tool(tool_name, params) messages.append({"role": "assistant", "tool_call": response.tool_call}) messages.append({"role": "tool", "content": json.dumps(result)}) else: return response.content return "达到最大轮次,任务未完成"这段代码比编排型的调度器还短,但它的行为完全不同:具体调哪个工具、按什么顺序调,全由模型决定。模型可能先调read_csv看列名,再调calc_average,最后调finish;也可能直接调calc_average并猜一个列名,失败了再重试。这种灵活性是编排型没有的,但不确定性也是编排型没有的。第三步:加护栏。运行时型 Agent 最大的风险是模型"跑偏",比如无限循环调用同一个工具、或者调用不存在的工具。所以要加护栏:最大轮次限制:max_turns防止无限循环。工具白名单:执行前检查工具名是否在注册表里。参数校验:用 schema 校验参数,不合法就返回错误让模型重试。重复调用检测:如果连续三次调用同一个工具且参数相同,强制中断。这些护栏在编排型里不太需要,因为流程是预设的;但在运行时型里是必须的,因为流程是模型生成的。4.3 两种架构的混合实践实际项目中,纯编排或纯运行时都少见,更多是混合。我做过一个"合同审查"的 Agent,就是混合架构:主流程用编排,子任务用运行时。主流程是固定的:上传合同 → 提取条款 → 逐条审查 → 生成报告。这四步用 DAG 编排,保证不会漏步骤。但"逐条审查"这一步,每条条款怎么审、要不要查法规、要不要对比模板,这些是动态的,就交给一个运行时型子 Agent 去处理。子 Agent 有自己的工具集(查法规库、查模板库、查历史案例),模型自主决定调哪些。这种混合架构的好处是兼顾了可控性和灵活性:主流程可控,保证任务完整;子任务灵活,适应条款的多样性。坏处是复杂度高,要维护两套调度逻辑,调试时要在两个层面之间切换。我的建议是:除非任务确实需要,否则不要轻易上混合架构,先用纯编排或纯运行时跑通,遇到瓶颈再混合。架构类型开发成本运行稳定性适应变化推荐场景纯编排中高低流程固定、需审计纯运行时低中高工具多变、快速迭代混合高中高中高主流程固定+子任务动态5. 常见问题与排查技巧实录5.1 任务卡死或死循环怎么排查这是编排型 Agent 最常见的问题。表现是任务一直不结束,日志停在某个节点不动。排查思路是先看 DAG 结构,再看节点状态。第一步,把任务图打印出来,检查有没有环。DAG 调度器最怕的就是隐式环,比如 A 依赖 B,B 依赖 C,C 又依赖 A。这种环在代码里可能不明显,但建图时会形成死锁。我一般会写一个validate_dag函数,在建图时就检测环:def validate_dag(nodes, deps): visited, stack = set(), set() def dfs(n): if n in stack: raise ValueError(f"检测到环,涉及节点:{n}") if n in visited: return stack.add(n) for d in deps.get(n, []): dfs(d) stack.remove(n) visited.add(n) for n in nodes: dfs(n)第二步,如果图没问题,就看节点状态。卡死通常是某个节点一直处于running状态,说明它的执行函数没有返回。可能是网络请求没设超时,可能是死循环,也可能是等一个永远不会来的锁。给每个节点执行加超时是最有效的预防手段,超时后标记为failed,让调度器继续往下走。提示:我习惯在节点执行函数外面包一层超时装饰器,默认 60 秒,特殊节点可以单独配置。这样即使某个节点出问题,也不会拖垮整个任务。5.2 模型不按预期调用工具怎么办运行时型 Agent 的典型问题。模型可能不调工具直接瞎编答案,也可能调错工具,还可能参数格式不对。排查要从工具描述和上下文两方面入手。工具描述的问题最常见。如果描述太模糊,模型就不知道什么时候该用。比如一个工具叫search,描述是"搜索信息",模型根本不知道搜什么、怎么搜。改成search_regulations,描述是"根据关键词搜索法规条文,返回匹配的条文列表",模型的调用准确率会明显提升。上下文的问题也常见。如果对话历史太长,模型可能"忘记"有哪些工具可用。这时候要在每轮对话前重新注入工具列表,或者把工具列表放在 system 消息里(system 消息通常不会被压缩掉)。还有一个技巧是给模型示例。在工具描述里加一句"例如:当用户问'XX 法规怎么规定'时,调用本工具,参数 keyword 填'XX'"。模型对示例的遵循度很高,这比纯文字描述有效得多。问题表现可能原因排查方法解决手段不调工具直接回答工具描述不清检查描述是否具体补充描述和示例调错工具工具名相似看调用日志重命名,增加区分度参数格式错schema 不明确看校验错误完善 schema,加类型说明重复调用上下文丢失看对话历史重新注入工具列表5.3 上下文超限怎么优雅处理上下文超限是长对话的必然问题。粗暴的做法是直接截断,但这样会丢失关键信息。优雅的做法是分层压缩。第一层,工具结果压缩。工具返回的大段数据,如果模型只是用来做判断,判断完了就不需要原文了。可以在工具结果后面加一个标记,下一轮对话时把标记过的结果替换成摘要。第二层,对话历史压缩。把超过 N 轮的对话用模型生成摘要,摘要里保留"用户的目标、已完成的步骤、当前的状态"这三个要素。第三层,系统提示压缩。system 消息里的工具列表和规则,如果太长,可以精简。但核心规则不能删,比如"不要编造工具调用"这种,删了模型就容易乱来。我实测下来,三层压缩一起用,能把一个 50 轮的对话压到原始长度的 20% 左右,同时保持任务连贯性。关键是压缩要在超限之前做,不要等报错了才处理。我一般设一个阈值,比如上下文用到 70% 就开始压缩,留 30% 的缓冲。5.4 工具执行失败的降级策略工具执行失败是常态,网络抖动、服务限流、参数错误都会导致失败。关键是失败后怎么办,不能让整个 Agent 崩掉。我的降级策略分三级:一级降级:重试。对于临时性错误(超时、限流),自动重试 2 到 3 次,退避时间递增。二级降级:替代。如果重试还失败,看有没有替代工具。比如主搜索工具挂了,用备用搜索工具。三级降级:告知。如果替代也没有,把失败信息返回给模型,让模型决定是换个方式还是告诉用户。这里的关键是失败信息要结构化,不能只返回一句"失败了"。要返回错误类型、错误码、建议的重试方式,模型才能做出合理决策。比如:{ "error": true, "type": "rate_limit", "message": "请求过于频繁,请稍后重试", "retry_after_ms": 5000, "suggestion": "等待 5 秒后重试,或改用备用工具" }模型看到这个,就知道该等 5 秒还是换工具,而不是瞎猜。6. 架构选型的个人经验与扩展思路聊了这么多架构细节,最后说点选型上的个人体会。我做过五六个 Agent 项目,踩过的最大坑不是技术问题,而是一开始就选错了架构方向。有个项目我一开始用编排型,把流程画得特别细,结果需求一变,整个图都要重画,改了两周才改完。后来换成运行时型,需求再变,加个工具就行,半天搞定。但另一个项目反过来,一开始用运行时型,模型老是漏步骤,后来改成编排型,把关键步骤固定下来,稳定性立刻上去了。所以我的经验是:先判断任务的确定性,再选架构。确定性高的(步骤可枚举、流程稳定)用编排,确定性低的(步骤动态、工具多变)用运行时。如果拿不准,先用运行时型快速验证,跑通了再看要不要把稳定部分固化成编排。扩展思路上,我觉得这两个项目都还没解决好的问题是多 Agent 协作。现在的架构都是单 Agent 的,一个模型从头干到尾。但真实任务往往需要多个角色,比如一个负责查资料、一个负责写文案、一个负责审核。多 Agent 协作涉及消息传递、任务分配、冲突解决,比单 Agent 复杂一个量级。我试过用编排型做多 Agent,把每个 Agent 做成一个节点,节点之间传消息,能跑通但效率不高。运行时型做多 Agent 更难,因为模型之间没有共享上下文。这块我还在摸索,有进展再分享。另外一个小技巧:给 Agent 加一个"思考日志"。让模型在每次工具调用前,先输出一段"我为什么要调这个工具"的思考。这段思考不一定要展示给用户,但存在日志里,调试时特别有用。我靠这个日志定位过好几次模型"跑偏"的原因,比看工具调用记录直观多了。
1. 从两个开源项目看 AI Agent 的架构分野第一次看到 OpenClaw 和 OpenCode 这两个名字放在一起,我的直觉是:这俩大概率是同一类东西的两种实现路径。实际拆下来发现,它们确实都在做"让 AI 模型自主调用工具完成任务"这件事,但架构取向几乎是两个极端。一个偏向"重编排、强约束",另一个偏向"轻内核、重扩展"。这种差异不是拍脑袋决定的,而是各自对"AI Agent 到底该长什么样"这个问题的不同回答。先把结论摆前面:OpenClaw 更像一个"任务编排框架",OpenCode 更像一个"能力运行时"。前者关心的是"一个复杂任务怎么被拆解、调度、回收",后者关心的是"一个模型怎么以最低成本接入各种工具并保持上下文连贯"。理解这条主线,后面所有的模块设计、通信机制、状态管理都能对上号。这篇文章适合三类人看:正在选型 Agent 框架的工程师、想自己撸一个 Agent 内核的开发者、以及被各种"Agent 架构图"绕晕的产品同学。我会尽量把每个设计决策背后的"为什么"讲清楚,而不是只罗列模块名。涉及具体参数和代码的地方,我会给出可直接参考的写法,但你要知道这些是基于常见实践的合理补全,不是官方文档的逐字搬运。2. 核心架构思路的拆解与选型逻辑2.1 两种架构哲学:编排优先 vs 运行时优先OpenClaw 的核心假设是:Agent 的复杂度主要来自任务本身。一个真实场景里的任务,比如"帮我分析这份销售数据并生成周报",它不是一次模型调用能搞定的,需要拆成读文件、算指标、写文案、格式化输出等多个步骤,步骤之间还有依赖关系。所以 OpenClaw 把大量精力花在任务图(Task Graph)的构建和调度上,模型只是图中的一个"决策节点"。OpenCode 的核心假设不一样:Agent 的复杂度主要来自工具生态的多样性。今天要调数据库,明天要调浏览器,后天要调某个内部 API,如果每接一个工具都要改框架,那这个框架就没法用。所以 OpenCode 把精力花在工具注册、上下文注入、流式响应这些"运行时能力"上,任务编排反而交给模型自己用自然语言去规划。这两种假设没有对错,只有适配场景的差异。我个人的经验是:流程相对固定、步骤可枚举的任务,用编排优先的架构更稳;流程高度动态、工具频繁变化的任务,用运行时优先的架构更灵活。下面这张表是我在实际选型时会对照的维度:对比维度OpenClaw(编排优先)OpenCode(运行时优先)任务定义方式显式任务图,节点可枚举隐式,模型自主规划工具接入成本需注册到编排层,改动较大插件式注册,改动小状态管理框架统一管理,可持久化依赖会话上下文,较轻调试难度图结构清晰,易定位依赖日志,链路较长适合场景固定流程、多步骤、需审计动态工具、快速迭代2.2 为什么会有这种分野:从"谁来做决策"说起要理解这两个项目的架构差异,得先回答一个更底层的问题:在一个 Agent 系统里,决策权到底在框架手里还是在模型手里?OpenClaw 的选择是"框架主导、模型辅助"。框架先根据任务类型生成一个任务图,每个节点是一个原子操作,模型只在需要"判断"的节点上被调用,比如"这个文件是 CSV 还是 Excel"、"这段文案的语气该正式还是轻松"。这样做的好处是可控性强——每一步做什么、做到哪了、失败了怎么回滚,框架都心里有数。坏处是灵活性差——如果任务超出了预设的图结构,框架就懵了。OpenCode 的选择是"模型主导、框架兜底"。框架只提供工具和上下文,具体调哪个工具、按什么顺序调,全由模型在对话中决定。框架的职责是保证工具调用不出错、上下文不丢失、流式输出不卡顿。好处是适应性强——模型能处理框架设计者没想到的任务组合。坏处是不确定性高——同样的输入,模型可能走出完全不同的路径,调试和复现都更麻烦。我踩过的一个坑是:早期用运行时优先的架构做一个"数据清洗"任务,模型有时候会跳过去重步骤直接输出,导致结果不稳定。后来改成编排优先,把去重做成强制节点,问题才解决。但反过来,做"客服问答"这种高度依赖上下文的任务时,编排优先的架构又显得太笨重,每次都要走一遍固定流程,响应慢且不自然。所以选型的第一原则是看任务的确定性程度,而不是看哪个框架"更先进"。2.3 架构分层:两者都绕不开的三层结构虽然取向不同,但 OpenClaw 和 OpenCode 在分层上其实有共识,都逃不开这三层:接入层:负责接收用户输入、管理会话、处理鉴权。这一层两者差异不大,都是标准的 Web 服务那一套。决策层:这是分歧最大的地方。OpenClaw 在这一层放了任务图调度器,OpenCode 在这一层放了模型对话管理器。执行层:负责实际调用工具、执行代码、访问外部资源。两者都采用"工具注册 + 统一调用接口"的模式,但 OpenClaw 的工具是绑定到任务节点的,OpenCode 的工具是全局可用的。这个三层结构不是谁规定的,而是任何 Agent 系统都必然要处理的三类问题:怎么进来、怎么决定、怎么执行。你在设计自己的 Agent 时,也可以先用这个框架去套,看每一层你打算放什么,放的东西是否符合你的任务特征。3. 核心模块的细节解析与实操要点3.1 任务调度模块:OpenClaw 的心脏OpenClaw 的任务调度模块是整个项目最值得细看的部分。它的核心是一个有向无环图(DAG)调度器,每个节点代表一个原子操作,边代表依赖关系。调度器的工作是:找到所有入度为 0 的节点,并行执行它们,执行完后更新下游节点的入度,直到所有节点执行完毕。这个设计的关键在于节点状态的精确管理。每个节点有五种状态:pending、running、success、failed、skipped。调度器在每一轮循环中做三件事:扫描所有pending且入度为 0 的节点,标记为running并提交执行。收集执行结果,成功的标记success,失败的标记failed。对于failed的节点,根据预设策略决定是重试、跳过还是终止整个图。这里有个容易忽略的细节:失败传播策略。如果一个节点失败了,它的下游节点应该怎么办?OpenClaw 默认是"下游全部标记skipped",但你可以配置成"下游继续执行,只是把失败信息作为输入传下去"。后者在"部分失败可容忍"的场景下很有用,比如批量处理 100 个文件,有 3 个失败了,你不想让剩下 97 个也停掉。实操中我建议你这样配置重试策略:retry_policy: max_attempts: 3 backoff: exponential base_delay_ms: 500 max_delay_ms: 8000 retry_on: [timeout, rate_limit, transient_error] abort_on: [auth_error, invalid_input]这个配置的意思是:最多重试 3 次,退避时间指数增长(500ms、1000ms、2000ms),只对超时、限流、临时错误重试,遇到鉴权失败或输入非法直接终止。为什么要区分retry_on和abort_on?因为有些错误重试一万次也没用,比如 API key 错了,你重试只是浪费时间和配额。这个区分能帮你省下大量无谓的等待。注意:DAG 调度器最怕的是"隐式依赖"。如果两个节点实际上有依赖关系,但你在建图时没连边,调度器就会并行执行它们,导致结果错乱。我的经验是:宁可多连边,不要少连边,串行执行慢一点,但结果是对的。3.2 工具注册与调用:OpenCode 的扩展点OpenCode 的工具注册机制是我见过比较优雅的设计。它用一个统一的Tool接口把所有能力抽象掉,每个工具只需要实现三个方法:class Tool: def name(self) -> str: """工具的唯一标识,模型通过这个名字调用""" pass def schema(self) -> dict: """参数的 JSON Schema,用于校验和生成模型可读的描述""" pass def execute(self, params: dict, context: dict) -> dict: """实际执行逻辑,context 里带着会话状态""" pass这个设计的精妙之处在于schema()方法。它不只是给框架做参数校验用的,还会被转换成模型能理解的工具描述,注入到对话上下文中。也就是说,你写一次 schema,模型就知道这个工具怎么用、参数是什么类型、哪些是必填的。这比手写工具描述文档靠谱多了,因为 schema 和实际执行逻辑是绑定的,不会出现"文档说参数是 string,实际要 int"这种破事。工具调用的完整链路是这样的:模型在对话中输出一个工具调用请求,格式是{"tool": "read_file", "params": {"path": "/data/a.csv"}}。框架拦截这个请求,先用schema()校验参数,不合法就直接返回错误给模型,让它重新生成。校验通过后,调用execute(),把结果序列化成 JSON。把结果作为一条tool_result消息追加到对话历史里,继续下一轮模型调用。这里有个实操要点:工具结果的体积控制。如果你调一个工具返回了 10MB 的数据,直接塞进上下文,模型要么超长截断,要么直接报错。OpenCode 的做法是给每个工具的结果设一个max_result_size,超过就截断并附上提示"结果已截断,共 X 字节,显示前 Y 字节"。我一般把这个值设在 8KB 到 32KB 之间,具体看模型上下文窗口大小。提示:工具命名尽量用"动词_名词"的格式,比如read_file、query_db、send_email。模型对这类命名的理解准确率明显高于fileReader或dbQuery这种驼峰式。这是我在多个项目里对比出来的经验,不是玄学。3.3 上下文管理:两者都头疼的问题上下文管理是 Agent 架构里最容易被低估、也最容易出问题的部分。OpenClaw 和 OpenCode 在这块的策略不同,但都绕不开三个核心问题:存什么、存多久、怎么取。OpenClaw 因为任务图是显式的,上下文管理相对简单:每个节点的输入输出都挂在节点上,节点执行完就把结果存到图的状态里,下游节点需要时直接从图里取。这种"图即上下文"的方式很清晰,但缺点是跨任务无法复用——上一个任务的中间结果,下一个任务拿不到。OpenCode 因为对话是连续的,上下文管理更接近传统的会话管理:维护一个消息列表,每条消息有角色(user/assistant/tool)和内容。随着对话轮次增加,消息列表会越来越长,最终超出模型上下文窗口。这时候就需要上下文压缩。我实测下来比较有效的压缩策略是"滑动窗口 + 摘要":保留最近 N 轮完整对话(N 一般取 6 到 10)。更早的对话,用模型生成一段摘要,替换掉原始消息。工具调用的结果,如果体积大且不再被引用,直接丢弃,只保留一句"曾调用 X 工具,结果已省略"。这个策略的关键参数是 N 和摘要长度。N 太小,模型会"失忆",忘记前面聊过什么;N 太大,压缩效果不明显。我的经验值是:对于 8K 上下文窗口的模型,N 取 6;对于 32K 的,N 取 10 到 15。摘要长度控制在 200 到 500 字之间,太短丢信息,太长又占空间。上下文策略适用场景优点缺点全量保留短对话、调试期信息完整很快超限滑动窗口长对话、生产环境实现简单早期信息丢失摘要压缩超长对话保留主线摘要本身有损图状态存储多步骤任务结构清晰跨任务不复用3.4 模型接入层:别小看这层的坑模型接入层看起来最简单——不就是调个 API 吗?但实际做下来,这层的坑一点不少。OpenClaw 和 OpenCode 都支持多模型接入,核心要处理的问题包括:流式响应、函数调用格式差异、错误重试、配额管理。流式响应是必须的,因为 Agent 的响应时间通常很长(要调工具、要等结果),如果不流式输出,用户会以为程序卡死了。但流式响应和函数调用有个冲突:函数调用的参数是流式返回的,你得等参数完整了才能执行。所以接入层要做一个缓冲,把流式的参数片段拼起来,拼完整了再触发工具调用。不同模型的函数调用格式差异也很大。有的用function_call字段,有的用tool_calls数组,有的把参数放在arguments里当字符串,有的直接给对象。接入层要做一个格式归一化,把各家格式统一成内部标准格式,上层逻辑就不用关心底层是哪个模型了。def normalize_tool_call(raw_call, provider): if provider == "provider_a": return { "name": raw_call["function"]["name"], "params": json.loads(raw_call["function"]["arguments"]) } elif provider == "provider_b": return { "name": raw_call["name"], "params": raw_call["input"] } # 其他 provider 的映射...这个归一化函数看着简单,但每接一个新模型就要加一个分支,维护成本不低。我的建议是:把映射关系写成配置,而不是硬编码在代码里,这样加新模型时改配置就行,不用改代码重新部署。注意:模型接入层一定要做超时和熔断。我遇到过某个模型服务商突然变慢,导致整个 Agent 卡住的情况。后来加了 30 秒超时和连续 5 次失败熔断,问题就没再出现过。熔断后要有一个恢复探测机制,比如每 60 秒试一次,成功了就恢复。4. 实操过程与核心环节实现4.1 从零搭一个最小可用的编排型 Agent光看架构不够,得动手。下面我用 OpenClaw 的思路,搭一个最小可用的编排型 Agent,任务是"读取一个 CSV 文件,计算某列的平均值,输出结果"。这个任务足够简单,但包含了编排型 Agent 的所有核心环节。第一步:定义任务图。这个任务可以拆成三个节点:read_csv:读取文件,输出原始数据。calc_avg:接收原始数据,计算指定列的平均值。format_output:接收平均值,格式化成自然语言。依赖关系是read_csv→calc_avg→format_output,一条链。第二步:实现节点执行器。每个节点是一个函数,接收上游的输出作为输入,返回自己的输出:def read_csv(inputs, context): path = context["params"]["file_path"] with open(path) as f: data = list(csv.DictReader(f)) return {"rows": data, "columns": list(data[0].keys())} def calc_avg(inputs, context): rows = inputs["read_csv"]["rows"] col = context["params"]["target_column"] values = [float(r[col]) for r in rows if r[col]] return {"average": sum(values) / len(values), "count": len(values)} def format_output(inputs, context): avg = inputs["calc_avg"]["average"] col = context["params"]["target_column"] return {"text": f"列 {col} 的平均值是 {avg:.2f}"}第三步:实现调度器。调度器负责按依赖顺序执行节点:def run_graph(graph, context): results = {} pending = set(graph.nodes.keys()) while pending: ready = [n for n in pending if all(d in results for d in graph.deps[n])] if not ready: raise RuntimeError("检测到循环依赖或死锁") for node in ready: inputs = {d: results[d] for d in graph.deps[node]} try: results[node] = graph.nodes[node](inputs, context) except Exception as e: results[node] = {"error": str(e)} pending.remove(node) return results这段代码不到 20 行,但已经是一个可用的 DAG 调度器了。关键点是ready的判断条件:所有依赖都在results里,说明上游都执行完了。如果某一轮ready为空但pending还有节点,说明有循环依赖,直接报错。第四步:接入模型。上面的节点都是纯函数,没有用到模型。如果要加一个"判断 CSV 的哪一列是数值列"的节点,就需要调模型:def detect_numeric_column(inputs, context): columns = inputs["read_csv"]["columns"] prompt = f"从这些列名中选出最可能是数值列的:{columns}" response = call_model(prompt) return {"column": response.strip()}这个节点插在read_csv和calc_avg之间,calc_avg的target_column就从context里取改成从inputs里取。这样整个流程就变成了"读文件 → 模型判断列 → 算平均 → 格式化",一个真正的 Agent 流程。4.2 从零搭一个最小可用的运行时型 Agent同样的任务,用 OpenCode 的思路来做,结构完全不同。运行时型 Agent 没有预设的任务图,只有一个对话循环和一组工具。第一步:定义工具。把读文件、算平均、格式化都做成工具:tools = [ { "name": "read_csv", "description": "读取 CSV 文件,返回列名和前几行数据", "params": {"path": "string, 文件路径"} }, { "name": "calc_average", "description": "计算指定列的平均值", "params": {"path": "string", "column": "string"} }, { "name": "finish", "description": "输出最终结果,结束任务", "params": {"text": "string, 最终答案"} } ]第二步:实现对话循环。核心逻辑是:把工具描述和用户问题一起发给模型,模型返回工具调用就执行,返回finish就结束:def run_agent(user_query, tools, max_turns=10): messages = [ {"role": "system", "content": build_tool_prompt(tools)}, {"role": "user", "content": user_query} ] for _ in range(max_turns): response = call_model(messages) if response.tool_call: tool_name = response.tool_call["name"] params = response.tool_call["params"] if tool_name == "finish": return params["text"] result = execute_tool(tool_name, params) messages.append({"role": "assistant", "tool_call": response.tool_call}) messages.append({"role": "tool", "content": json.dumps(result)}) else: return response.content return "达到最大轮次,任务未完成"这段代码比编排型的调度器还短,但它的行为完全不同:具体调哪个工具、按什么顺序调,全由模型决定。模型可能先调read_csv看列名,再调calc_average,最后调finish;也可能直接调calc_average并猜一个列名,失败了再重试。这种灵活性是编排型没有的,但不确定性也是编排型没有的。第三步:加护栏。运行时型 Agent 最大的风险是模型"跑偏",比如无限循环调用同一个工具、或者调用不存在的工具。所以要加护栏:最大轮次限制:max_turns防止无限循环。工具白名单:执行前检查工具名是否在注册表里。参数校验:用 schema 校验参数,不合法就返回错误让模型重试。重复调用检测:如果连续三次调用同一个工具且参数相同,强制中断。这些护栏在编排型里不太需要,因为流程是预设的;但在运行时型里是必须的,因为流程是模型生成的。4.3 两种架构的混合实践实际项目中,纯编排或纯运行时都少见,更多是混合。我做过一个"合同审查"的 Agent,就是混合架构:主流程用编排,子任务用运行时。主流程是固定的:上传合同 → 提取条款 → 逐条审查 → 生成报告。这四步用 DAG 编排,保证不会漏步骤。但"逐条审查"这一步,每条条款怎么审、要不要查法规、要不要对比模板,这些是动态的,就交给一个运行时型子 Agent 去处理。子 Agent 有自己的工具集(查法规库、查模板库、查历史案例),模型自主决定调哪些。这种混合架构的好处是兼顾了可控性和灵活性:主流程可控,保证任务完整;子任务灵活,适应条款的多样性。坏处是复杂度高,要维护两套调度逻辑,调试时要在两个层面之间切换。我的建议是:除非任务确实需要,否则不要轻易上混合架构,先用纯编排或纯运行时跑通,遇到瓶颈再混合。架构类型开发成本运行稳定性适应变化推荐场景纯编排中高低流程固定、需审计纯运行时低中高工具多变、快速迭代混合高中高中高主流程固定+子任务动态5. 常见问题与排查技巧实录5.1 任务卡死或死循环怎么排查这是编排型 Agent 最常见的问题。表现是任务一直不结束,日志停在某个节点不动。排查思路是先看 DAG 结构,再看节点状态。第一步,把任务图打印出来,检查有没有环。DAG 调度器最怕的就是隐式环,比如 A 依赖 B,B 依赖 C,C 又依赖 A。这种环在代码里可能不明显,但建图时会形成死锁。我一般会写一个validate_dag函数,在建图时就检测环:def validate_dag(nodes, deps): visited, stack = set(), set() def dfs(n): if n in stack: raise ValueError(f"检测到环,涉及节点:{n}") if n in visited: return stack.add(n) for d in deps.get(n, []): dfs(d) stack.remove(n) visited.add(n) for n in nodes: dfs(n)第二步,如果图没问题,就看节点状态。卡死通常是某个节点一直处于running状态,说明它的执行函数没有返回。可能是网络请求没设超时,可能是死循环,也可能是等一个永远不会来的锁。给每个节点执行加超时是最有效的预防手段,超时后标记为failed,让调度器继续往下走。提示:我习惯在节点执行函数外面包一层超时装饰器,默认 60 秒,特殊节点可以单独配置。这样即使某个节点出问题,也不会拖垮整个任务。5.2 模型不按预期调用工具怎么办运行时型 Agent 的典型问题。模型可能不调工具直接瞎编答案,也可能调错工具,还可能参数格式不对。排查要从工具描述和上下文两方面入手。工具描述的问题最常见。如果描述太模糊,模型就不知道什么时候该用。比如一个工具叫search,描述是"搜索信息",模型根本不知道搜什么、怎么搜。改成search_regulations,描述是"根据关键词搜索法规条文,返回匹配的条文列表",模型的调用准确率会明显提升。上下文的问题也常见。如果对话历史太长,模型可能"忘记"有哪些工具可用。这时候要在每轮对话前重新注入工具列表,或者把工具列表放在 system 消息里(system 消息通常不会被压缩掉)。还有一个技巧是给模型示例。在工具描述里加一句"例如:当用户问'XX 法规怎么规定'时,调用本工具,参数 keyword 填'XX'"。模型对示例的遵循度很高,这比纯文字描述有效得多。问题表现可能原因排查方法解决手段不调工具直接回答工具描述不清检查描述是否具体补充描述和示例调错工具工具名相似看调用日志重命名,增加区分度参数格式错schema 不明确看校验错误完善 schema,加类型说明重复调用上下文丢失看对话历史重新注入工具列表5.3 上下文超限怎么优雅处理上下文超限是长对话的必然问题。粗暴的做法是直接截断,但这样会丢失关键信息。优雅的做法是分层压缩。第一层,工具结果压缩。工具返回的大段数据,如果模型只是用来做判断,判断完了就不需要原文了。可以在工具结果后面加一个标记,下一轮对话时把标记过的结果替换成摘要。第二层,对话历史压缩。把超过 N 轮的对话用模型生成摘要,摘要里保留"用户的目标、已完成的步骤、当前的状态"这三个要素。第三层,系统提示压缩。system 消息里的工具列表和规则,如果太长,可以精简。但核心规则不能删,比如"不要编造工具调用"这种,删了模型就容易乱来。我实测下来,三层压缩一起用,能把一个 50 轮的对话压到原始长度的 20% 左右,同时保持任务连贯性。关键是压缩要在超限之前做,不要等报错了才处理。我一般设一个阈值,比如上下文用到 70% 就开始压缩,留 30% 的缓冲。5.4 工具执行失败的降级策略工具执行失败是常态,网络抖动、服务限流、参数错误都会导致失败。关键是失败后怎么办,不能让整个 Agent 崩掉。我的降级策略分三级:一级降级:重试。对于临时性错误(超时、限流),自动重试 2 到 3 次,退避时间递增。二级降级:替代。如果重试还失败,看有没有替代工具。比如主搜索工具挂了,用备用搜索工具。三级降级:告知。如果替代也没有,把失败信息返回给模型,让模型决定是换个方式还是告诉用户。这里的关键是失败信息要结构化,不能只返回一句"失败了"。要返回错误类型、错误码、建议的重试方式,模型才能做出合理决策。比如:{ "error": true, "type": "rate_limit", "message": "请求过于频繁,请稍后重试", "retry_after_ms": 5000, "suggestion": "等待 5 秒后重试,或改用备用工具" }模型看到这个,就知道该等 5 秒还是换工具,而不是瞎猜。6. 架构选型的个人经验与扩展思路聊了这么多架构细节,最后说点选型上的个人体会。我做过五六个 Agent 项目,踩过的最大坑不是技术问题,而是一开始就选错了架构方向。有个项目我一开始用编排型,把流程画得特别细,结果需求一变,整个图都要重画,改了两周才改完。后来换成运行时型,需求再变,加个工具就行,半天搞定。但另一个项目反过来,一开始用运行时型,模型老是漏步骤,后来改成编排型,把关键步骤固定下来,稳定性立刻上去了。所以我的经验是:先判断任务的确定性,再选架构。确定性高的(步骤可枚举、流程稳定)用编排,确定性低的(步骤动态、工具多变)用运行时。如果拿不准,先用运行时型快速验证,跑通了再看要不要把稳定部分固化成编排。扩展思路上,我觉得这两个项目都还没解决好的问题是多 Agent 协作。现在的架构都是单 Agent 的,一个模型从头干到尾。但真实任务往往需要多个角色,比如一个负责查资料、一个负责写文案、一个负责审核。多 Agent 协作涉及消息传递、任务分配、冲突解决,比单 Agent 复杂一个量级。我试过用编排型做多 Agent,把每个 Agent 做成一个节点,节点之间传消息,能跑通但效率不高。运行时型做多 Agent 更难,因为模型之间没有共享上下文。这块我还在摸索,有进展再分享。另外一个小技巧:给 Agent 加一个"思考日志"。让模型在每次工具调用前,先输出一段"我为什么要调这个工具"的思考。这段思考不一定要展示给用户,但存在日志里,调试时特别有用。我靠这个日志定位过好几次模型"跑偏"的原因,比看工具调用记录直观多了。