“太空计划”是我最近给自己的 Agent 项目起的代号,目标很朴素:把一套太空观测数据管线(轨道根数、可见窗口预报、观测任务记录)从“人工翻资料、手动跑脚本、再复制结果给模型”的流程,改成由 AI Agent 自主完成“理解需求—调工具—算结果—给结论”的闭环。而让这个项目真正跑得顺的关键,是我把底层数值计算放进了 Mojo,用 Python 做 LLM 调度和 Agent 状态管理。这篇文章我把整个方案拆开讲清楚:为什么这样分层、Mojo 工具层怎么写、ReAct 循环怎么接线、我自己踩了哪些坑,以及这套项目经历在 Agent 面试里怎么讲最有说服力。
1. 为什么我把“重型计算”放进 Mojo,而不是让 Agent 全部用 Python 跑
1.1 先给这颗 Agent 划任务边界
很多 Agent 项目翻车,不是模型不够聪明,而是把什么活都扔给 Python 主进程干。规划、记忆、工具调度、数值计算、字符串清洗全部挤在一个进程里,看起来方便,一旦任务量上来,Latency 就变得不可控。
我在“太空计划”里对 Agent 的定位是“任务编排者”,不是“数值计算器”。它要理解自然语言指令,决定调用哪个观测窗口计算模块,再把结果整理成人类能读懂的结论。真正的轨道预报、窗口交集判断、单位换算这些高频数值操作,属于工具层职责,不需要模型参与。这类计算有两个特点:
- 计算密度高,一个窗口筛选往往要循环几千条观测记录;
- 对精度和参数校验要求严格,单位错了、时间格式错了,结果全盘废掉。
Mojo 刚好在这个位置有优势。它保留了类似 Python 的叙事语法,同时通过fn这样的严格函数形态和静态类型让热点代码可以被编译优化。我在项目里把工具层编译成独立模块,Python harness 通过 import 调用,既没有丢掉 Agent 生态的便利性,又拿到了实打实的性能余量。
1.2 第一个 Mojo 核心模块:轨道预报与窗口交集
项目第一部分,我先用 Mojo 写了两个最常用的工具函数:圆形轨道速度计算和观测窗口交集判断。代码如下:
# mojo/tools/astro_tools.mojo from math import sqrt fn orbital_velocity(mu_km3_s2: F64, radius_km: F64) -> F64: """圆形轨道速度,输入引力常数和轨道半径,返回 km/s""" return sqrt(mu_km3_s2 / radius_km) fn windows_overlap( a_start: F64, a_end: F64, b_start: F64, b_end: F64 ) -> Bool: """判断两个观测时间窗口是否存在交集""" return (a_start < b_end) and (b_start < a_end)windows_overlap看着简单,但它决定了 Agent 的一个关键能力:AI 从文本里提取出两个时间段后,能不能正确判断“这个卫星过境时段和地面站可用窗口”有没有重叠。模型本身做这种时间区间判断很容易犹豫,交给确定性工具则永远稳定。
我刻意没有用 Python 写这个模块。原因很实在:窗口交集判断一旦做成“给 Agent 调用几十次的工具”,运行次数会非常频繁,并且每一轮循环都产生跨语言调用开销。用 Mojo 把这部分算稳,等于把整条链路上最容易被拖慢的一段肌肉练结实了。
1.3 互操作骨架:Python 负责 LLM 和状态,Mojo 负责数值与校验
“太空计划”并没有把 Mojo 用作 Agent 全栈,它只承担“能力提供方”。整体互操作骨架分三层:
- 最外层是 Python harness,负责 LLM API 调用、ReAct 循环、Agent 记忆维护、工具分发;
- 中间层是工具注册表,用 Python 字典维护工具名、描述和入口函数;
- 最内层是 Mojo 编译产物,供 Python 直接导入,内部做数值计算和参数合法性检查。
这样做的好处我体会很深:LLM 领域相关库、Prompt 管理、JSON 序列化这些生态在 Python 里太成熟了,没必要用新语言硬造轮子。而 Mojo 作为被调用的模块,也不需要关心模型是什么、Prompt 是什么,它只保证“你给我的参数是对的,我就能快速算对”。职责一旦切清,两边开发效率都高。
2. 太空计划 Agent 的整体架构:状态、技能注册表与工具边界
2.1 最小可用 ReAct 循环,代码不到 80 行
AI Agent 的架子看起来神秘,拆开就是“思考—行动—观察”三步循环。我给“太空计划”设计了一个最小 ReAct(Reasoning + Acting)循环,完整代码不到 80 行:
# harness/react_loop.py def run_agent(question: str, max_step: int = 6) -> str: trace = [] step = 0 while step < max_step: # 把对话历史、可观察轨迹、用户问题一起交给模型 prompt = build_planner_prompt( user_question=question, trace=trace, memory=memory_store.snapshot(), ) raw = llm.chat(prompt) plan = parse_json(raw) # 强制 JSON 结构化输出 if plan.get("finish"): return plan["final_answer"] tool_name = plan["tool"] arguments = plan["arguments"] try: result = dispatcher.execute(tool_name, arguments) observation = f"{tool_name} 返回:{result}" except Exception as e: observation = f"{tool_name} 执行失败:{str(e)}" trace.append(observation) step += 1 return "已达到最大步数,请改需求或检查工具定义。"这段代码是 Agent 项目的骨架,后面几乎一切功能都是在给这个循环填充细节:工具多了需要路由,记忆长了需要裁剪,错误多了需要恢复策略。ReAct 最大的价值在于“所见即所得”,每一步的工具返回都会拼进下一轮 Prompt,模型能看到自己之前做了什么,这种自校正能力是单次大 Prompt 不可能具备的。
我建议所有准备做 Agent 开发的新人,都先把这种最小循环跑通,再谈复杂编排。面试里被问“Agent 的本质是什么”,能画出这个循环,比背一百个概念强得多。
2.2 Skill 和 Agent 的边界:我最后是怎么划分的
“Skill 和 Agent 的区别”是 Agent 面试里的高频问题之一,我在项目里也真实体会到了这种区别。Skill 是“能力原子”,它只回答“我会做什么”的问题;Agent 是“决策实体”,它回答“我现在要不要做、怎么做、做错了怎么办”。
我在工具注册表里维护的大多数是 Skill,比如ephemeris_query(星历查询)、window_calc(窗口计算)、unit_convert(单位转换)。Agent 不会直接跟这些 Skill 的细节绑定,它只维护一份工具清单和触发规则。当收到“明天上午能不能观测到某颗低轨卫星”这类问题时,Agent 的第一步是拆解问题,然后决定是否调用多个 Skill 协作。
所以我的划分方法是:
- 一个可复用的独立函数,提升为 Skill;
- 一组 Skill 加上决策状态、记忆、目标拆解逻辑,才是 Agent;
- Prompt 中只给 Agent 暴露 Skill 的描述和参数示例,不给具体内部实现。
2.3 工具注册表:避免把框架写成“硬编码分支”
工具数量一多,很容易写出一长串if tool_name == "xxx"的判断。这种方式最大问题是新增工具时必须改代码、重启进程,而且工具描述散落在分支里,模型没法动态发现新能力。
我改成基于装饰器的注册表写法:
# harness/tool_registry.py TOOL_REGISTRY = {} def register_tool(name: str, description: str): def deco(fn): TOOL_REGISTRY[name] = { "fn": fn, "description": description, "schema": getattr(fn, "schema", None), } return fn return deco @register_tool( name="query_orbit_data", description="按卫星名称查询轨道根数,输入格式:{'sat_name': str}", ) def query_orbit_data(sat_name: str): return space_tools.fetch_orbit(sat_name)这样工具的元信息集中、可枚举,给模型生成工具列表的时候,直接遍历TOOL_REGISTRY,把 name 和 description 拼进 Prompt 即可。框架本身不关心新增的工具内部实现,只管调度,架构弹性立刻上来了。
2.4 Agent 记忆:短期窗口与长期存档的分工
Agent 记忆是现在的热门话题,我在“太空计划”里把记忆拆成两层。
短期记忆存当前 ReAct 循环里产生的观察记录,它直接影响下一步决策。为了防止上下文膨胀,我通常会保留最近 5 轮 trace,更早的会被压缩成一句话摘要。长期记忆承担“跨会话复用”功能,比如用户经常查询某颗特定卫星,我会把历史查询条件、关注的轨道高度范围写入本地 JSONL 文件,下次对话开始前加载相关条目。
记忆的设计原则是:不追求“把一切记住”,只求“在正确的时候把最相关的信息送回上下文”。一次对话里塞大量不相关历史,反而会稀释模型对当前问题的注意力。这也是很多 Agent 做不好,导致用户觉得“它记不住东西”的常见原因。
3. 规划-执行-反馈闭环里,Mojo 工具层的实现细节
3.1 Planner 的结构化输出设计
Agent 的规划部分本质是让模型输出可解析的动作序列。我在实践中坚持让模型输出严格 JSON,而不是自由文本。因为自由文本的 executability 太低,解析失败率会显著拖垮整个项目的稳定性。我给 Planner 的 Prompt 里固定了下面这种格式:
请严格输出 JSON,不要输出任何解释。 格式示例: { "thought": "用户要查询近地轨道卫星A在今晚的可见窗口", "tool": "query_orbit_data", "arguments": {"sat_name": "A"}, "finish": false } 如果已经收集足够信息,请输出: { "thought": "已获得卫星A的轨道数据,可以给出结论", "final_answer": "今晚20:14至20:26可观测,最佳仰角32度。", "finish": true }我在 Harness 端还加了一个parse_json的容错处理:如果模型输出的 JSON 前后混入了 Markdown 代码块标记,就剥掉再解析;如果解析失败,把“输出必须是合法 JSON”作为错误观察反馈给模型,让它重新生成。这种“用反馈代替崩溃”的思路,在 Agent 开发里几乎到处都用得上。
3.2 Mojo 工具层代码拆解:参数校验比计算本身更重要
Mojo 工具层真正让我受益的点,不只是算得快,是它的类型签名强制我把边界想清楚。比如窗口计算,如果调用方传进来一个结束时间早于开始时间的区间,算出来的交集没意义。我会在 Mojo 里直接做防御式校验:
fn validate_window(start: F64, end: F64, label: String) -> Bool: if start >= end: print("非法窗口:", label, "起始时间必须早于结束时间") return False return True这里有个容易被忽略的经验:Agent 调用工具产生的错误,最好在工具层就拦截掉,不要让一层带着错误数据往下算。模型提取时间参数时,可能因为用户口语表达产生“晚上 8 点到 6 点”这种反直觉区间,工具层通过校验返回错误信息,比模型自己硬猜靠谱得多。
我还用 Mojo 的结构体封装过一批“应用层返回对象”,把工具执行结果、耗时、错误码一起打包,方便 Harness 判断下一步动作。虽然跨语言传递对象会增加一点序列化成本,但对比计算正确性的提升,完全值得。
3.3 反馈回路与错误恢复:遇到“agent execution terminated”时的处理
我开发过程中最难受的报错之一,就是模型侧返回agent execution terminated due to error,整个会话直接结束,用户看不到任何中间状态。这类错误大多来自两种情况:
- 工具调用参数非法,模型不知道该怎么纠正,触发了循环终止;
- Harness 端的外部 API 超时,没有把超时原因转化为可观察反馈。
我的处理办法是在dispatcher.execute里把异常全部接住,并转成一条普通观察文本:
observation = f"工具执行失败:{type(e).__name__}: {str(e)}。请检查参数是否符合schema。"这样模型在下一次思考时能看到失败原因,有机会修正参数,而不是直接让 Agent 程序崩溃。同时我会设置单步超时时间,比如工具执行超过 5 秒就返回“工具超时”,提醒模型换一种查询策略。整套“错误转观察”的机制,让“太空计划”在真实数据上的连续任务成功率明显提高。
3.4 传递 JSON 的三种姿势与性能取舍
Mojo 工具层与 Python Harness 之间的数据交换,我试过三种方案:
- 直接返回字符串:最简单,但带着大量 JSON 解析工作回到 Python;
- 返回预格式化字符串:适合计算结论直接打进观察结果;
- 由 Harness 统一做
json.dumps:适合把 Mojo 数值结构先转成 dictionary 再加工具元信息。
实际项目里我主要用第三种方案。Mojo 负责计算,Python 负责“对外表演”,两边各取所长。性能上,每次循环的 JSON 序列化开销不大,真正昂贵的是模型推理时间。所以没必要为了节约序列化时间去牺牲代码清晰度,这个点很多人容易本末倒置。
4. 把“太空计划”当试验田:我实测得到的结论和踩过的坑
4.1 Mojo 加速效果数据对比
我拿项目里最典型的场景做了个对比测试:处理一条包含 2000 个候选观测时间片的可见窗口筛选任务,分别用纯 Python 循环和 Mojo 工具层实现,统计总耗时。Mojo 侧包含了一次传参校验、窗口交集判断和结果聚合,Python 侧用常规datetime遍历。
| 实现方式 | 2000 条时间片处理耗时 | 备注 |
|---|---|---|
| Python 循环 | 约 280ms | 含对象创建和多次函数调用 |
| Mojo 工具层 | 约 45ms | 含参数校验和结构化返回 |
需要说明,这不是严格基准测试,只是项目实际调用里的观察值。但它对我的决策影响很大:如果 Agent 内部规划需要反复调用这个工具,一次任务多轮循环下来,Mojo 版本省下的时间够模型再思考一步,对用户体验的提升非常明显。
4.2 “安装失败、无法接收检测信号”这类报错给我的启发
开发“太空计划”期间,我还处理过一个和 LLM Agent 完全无关的报错:安装某个运维组件时提示“安装失败。无法接收 agent 发出的检测信号。请确保主机的名称已正确配置。”乍一看和 AI Agent 毫不相干,但它让我重新思考了 Agent 工程里的“注册与心跳”机制。
运维场景里的 agent,需要定时向服务端发送心跳确认自己存活;如果主机名解析错误或网络隔离,服务端就收不到信号。LLM Agent 项目里其实也有类似概念——Harness 需要在每次工具调用前确认工具模块是否可用、依赖是否加载成功,否则模型会对着一个根本不存在的工具名硬编参数。我在工具注册表里加了一层“可用性检查”,每次循环开始前快速确认 Mojo 模块能正常导入,避免跑了一半才发现底层编译产物缺失。这跟“主机名没配对导致服务端收不到检测信号”是同一个道理:检查前置条件,才能避免后续流程空转。
4.3 回看高频 Agent 面试题,这样答最有说服力
网上流传的 “agent 八股”“agent 面试题”很多,我结合这次项目经验总结出三条回答主线。
- 问“Agent 和 Workflow 的区别”:我会说 Workflow 是预设轨道,Agent 是场景内自主决策的驾驶舱,轨道可以帮你稳定到达,但遇到新情况需要 Agent 自己换路。
- 问“Harness 和 Agent 的区别”:Harness 是脚手架,是外壳,负责把模型、工具、记忆、回调组装起来;Agent 是壳内那个做推理决策的核心对象。面试官要听的是你有没有“层”的思维,而不是背定义。
- 问“Skill 和 Agent 的区别”:我会拿“太空计划”举例,
window_calc是 Skill,像个计算器;Agent 知道什么时候该掏出这个计算器、按哪个键、把结果贴给用户。能力与决策分离,才是 Agent 工程化的关键。
另外,类似“前端转 Agent 开发”的路径,我建议先从一个具体垂直场景入手,把工具层做扎实,再扩展到多轮对话和记忆。别一上来就搭通用框架,通用框架在真实问题面前往往反而最无力。
5. 从单 Agent 到多 Agent,我的扩展计划
5.1 下一步:编排层与 Router
“太空计划”现在是单 Agent 完成全部决策,后续我会引入一个 Router 层,先根据问题类型把任务路由给数据查询 Agent、计算 Agent 或报告 Agent。这样每个子 Agent 的工具列表更短,Prompt 更聚焦,出错面更小。想到这我就理解了为什么很多 Agent 框架强调“Agent 编排”:单 Agent 是全能助手,多 Agent 是专家委员会,委员会能否协调好,核心看 Router 的分发和上下文传递。
5.2 我对 Mojo 在 Agent 工程中位置的判断
经历这次项目,我认为 Mojo 更适合当 Agent 的“肌肉系统”,而不是“神经系统”。神经系统负责感知和决策,适合模型和成熟的 Python 生态;肌肉系统负责高频率、高强度数据处理,适合 Mojo 这种编译型模块。未来 Agent 应用要支撑大规模实时数据处理,一定需要把热点工具下沉到编译层。谁先把这块做好,谁就能在实际项目里获得明显的性能和成本优势。
5.3 如果你想照着做,我的实用建议
最后给想动手的朋友几条直接建议:
- 先跑通最小闭环,再谈架构。我第一版“太空计划”就是写死两个工具的标准 ReAct 循环,跑顺了才改注册表。
- 不要把 Mojo 当成万能替换品。它的强项在数值工具和编译优化,不适合硬写 Agent 状态机。
- 每个工具都要有清晰的错误返回。项目里 80% 的失败源于工具层没有给出可操作的错误反馈,而不是模型不会推理。
- 记忆系统越早设计越好。后期补记忆,要付出的代价是重构 Harness 的上下文组装逻辑。
我自己现在维护这个项目时,经常提醒自己一句话:不要让 Agent 看起来忙,要让 Agent 每一步都有效。Mojo 工具层的价值,恰好就是让那些“基础但高频”的能力又快又稳,把更多算力留给真正需要模型判断的部分。如果你也在做自己的 Agent 项目,不妨也给它起个响亮代号,然后用一个小而实的场景把它打磨到极致。