基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
AIHT
内容
精选全部 AI 动态热点榜AI 日报收藏
分类
模型行业编程产品技术观点开源
更多
Agent 接入
返回首页开发者导航
AIHT
精选 / 资讯详情
8.0
热点
AI SCORE
技术实践2026-10-31 13:23

用约200行TypeScript搭建智能体执行循环

dev.to · AI#Agent#TypeScript#框架选型
Editor brief · 编辑速览

文章提出以执行循环、工具注册表、三项限制和审批关卡构建轻量智能体,并强调校验模型输出。持久化运行、多智能体路由和可观测性需求出现后,再评估引入框架。

文章思维导图
Knowledge map
拖拽缩放
AI deep read

AI 深度解读

一个只接几个工具的 AI Agent,真的需要先装 LangChain 或 LangGraph 吗?作者的答案是:先用 200 多行 TypeScript 写清执行循环,把停止条件、工具权限和结果检查握在自己手里,等需求明确了再引入框架。

这篇最有用的地方,是把 Agent 拆成了能逐项检查的代码。但它证明的是模拟模型下的控制逻辑,距离真实业务运行还有缺口。尤其是费用统计、参数校验和人工审批,不能因为示例里出现了对应字段,就认为已经做好了。

Agent 的核心,是谁决定下一步能不能执行

作者把 Agent 的运行过程压缩成一个循环:把消息发给模型,执行模型请求的工具,把结果送回去;模型不再请求工具时,检查答案并结束。

围绕这个循环,程序需要六部分:保存消息的主循环、登记工具的注册表、限制执行的规则、隔离供应商差异的模型接口、检查最终答案的验证器,以及拦住高风险操作的审批入口。所谓 harness,可以理解为模型外面的执行控制程序:模型提议做什么,由它决定实际执行什么。

几个类型划清了责任。Message 保存运行历史;ToolDef 集中定义工具名称、描述、参数 schema、needsApproval 和处理函数;Limits 放置停止与校验规则。模型只需实现 ModelClient.complete(),业务工具不用跟着供应商 SDK 改写。

作者还主动选择串行执行工具。并行通常更快,但串行更容易追踪结果和错误的先后顺序,也便于排查工具之间的依赖。这个取舍偏向可读性,性能是否够用仍要看任务。

文章引用了 InfoQ 在 2026 年 10 月 26 日对 agentic harnesses 的讨论,以及 Anthropic 同月关于主循环与渐进式工具披露的观点,用来支持「先选择满足要求的最简单设计」。这些属于作者引用的外部论据,并不是这份代码的测试结果。

三道限制怎么生效,代码里又漏了什么

作者设置了三道限制:maxTurns 限制模型调用轮数,maxCostUsd 限制累计费用,重复调用检查拦住连续相同的工具请求。写操作再通过 needsApproval 单独审批。

下面这段原始代码展示了整个控制过程,也暴露了限制实际生效的位置:

export async function runAgent(
  task: string,
  model: ModelClient,
  tools: ToolDef[],
  limits: Limits,
): Promise<AgentResult> {
  const messages: Message[] = [{ role: "user", content: task }];
  const registry = new Map(tools.map((t) => [t.name, t]));
  let turns = 0;
  let costUsd = 0;
  let lastCall: ToolCall | null = null;
  while (true) {
    if (turns >= limits.maxTurns) {
      return { status: "turn_limit", turns, costUsd };
    }
    if (costUsd >= limits.maxCostUsd) {
      return { status: "cost_limit", turns, costUsd };
    }
    const res = await model.complete(messages, tools);
    turns += 1;
    costUsd += res.costUsd;
    messages.push({ role: "assistant", content: res.text, toolCalls: res.toolCalls });
    if (res.toolCalls.length === 0) {
      if (limits.verify && !limits.verify(res.text)) {
        return { status: "failed_verification", turns, costUsd };
      }
      return { status: "done", answer: res.text, turns, costUsd };
    }
    for (const call of res.toolCalls) {
      if (lastCall && sameCall(lastCall, call)) {
        return { status: "repeated_call", turns, costUsd };
      }
      lastCall = call;
      const tool = registry.get(call.name);
      if (!tool) {
        messages.push({ role: "tool", toolCallId: call.id, content: `Unknown tool: ${call.name}` });
        continue;
      }
      if (tool.needsApproval) {
        const ok = limits.approve ? await limits.approve(call.name, call.args) : false;
        if (!ok) {
          return { status: "blocked", turns, costUsd };
        }
      }
      try {
        const result = await tool.run(call.args);
        messages.push({ role: "tool", toolCallId: call.id, content: JSON.stringify(result) });
      } catch (err) {
        messages.push({ role: "tool", toolCallId: call.id, content: `Tool error: ${(err as Error).message}` });
      }
    }
  }
}

要盯住 costUsd += res.costUsd 这一行:费用在模型调用完成后才累计,预算检查却在下一轮开头。单次请求可能越过预算;如果这一轮直接返回最终答案,还可能以 done 结束。它提供的是轮次之间的费用检查,不能保证账单绝不超过上限。

重复检查也有明确边界。sameCall() 比较工具名称和参数的 JSON.stringify() 结果,只能拦住连续相同的调用,挡不住 A、B、A、B 交替循环。参数对象的键顺序不同,也可能影响比较结果。

有两处处理值得保留:未知工具和工具执行异常都会变成工具消息,给模型纠正机会;需要审批却没有提供 approve 时,默认返回 blocked。

不过,模型请求异常,以及适配器里的 JSON 解析异常,并没有被这段工具执行的 try/catch 接住,仍可能直接抛出。

接上真实模型,还差费用、校验和业务规则

作者提供了基于 fetch 的 OpenAI 兼容适配器,通过 baseUrl、apiKey 和 model 配置供应商,默认地址为 https://api.openai.com/v1,请求路径是 /chat/completions。这能隔离接口转换逻辑,但适用前提是对方支持相应的 Chat Completions 结构。

示例最大的缺口是 costUsd: 0。作者明确承认,示例没有根据 data.usage 和各模型价格表计算费用;作者称自己的运行会补上这一步,但本文没有展示。

直接复制这份适配器,费用上限不会发挥作用。

参数处理同样没有完成。JSON.parse() 只能解析 JSON,不能保证 orderId 存在、类型正确,或者订单确实允许退款。作者建议在每个工具的 run() 内校验,或在注册表外统一增加 schema 检查,并提到自己使用的 Zod 模式。

退款示例包含读取工具 get_order_status 和写入工具 issue_refund,使用 gpt-5-mini、OPENAI_API_KEY,设置 maxTurns: 12、maxCostUsd: 0.5。目标订单是 ord_f6g7h8i9j0,初始状态为 processing;另一个订单 ord_a1b2c3d4e5 已经是 refunded。

审批通过 APPROVE_WRITES === "yes" 判断。这能演示审批入口,却没有实现逐次人工确认。最终验证器 answer.includes("ord_") 也只检查文本中是否含有该片段,不能证明订单正确、退款成功。

还有一个容易忽略的点:issue_refund 的描述要求先确认状态,但处理函数直接修改订单。工具描述给模型提供指引,业务前置条件仍需代码执行。

我的判断

我认同作者对单 Agent、小工具集的取舍。一个 Agent 配 5 个工具时,先把执行路径写清楚,通常更容易看懂失败原因。作者认为多数 Agent 不会超过 10 个工具,但没有给出统计依据,这应当视为经验判断。

作者实际使用脚本化的模拟模型,验证了轮数限制、费用限制、第二次连续相同调用、未批准写入,以及缺少订单标识时的验证失败。本文没有用真实模型和真实费用完成测试,也没有提供与框架方案的成功率、成本或延迟对照数据。

因此,200 多行代码证明的是基础控制流程可以很小,并没有证明生产运行的全部问题已经解决。最终答案检查也不会撤销此前执行过的写操作,关键业务条件必须在工具内部验证。

框架的价值取决于需求。如果第一天就要求重启后恢复、跨天重试与检查点、多个 Agent 共享状态,或者需要现成的可观测性和工具生态,作者也支持直接使用框架或托管运行时。团队已经熟悉框架、交付时间紧,同样是合理前提。

原文在「四种情况」之后缺失了具体列表,后续段落和 FAQ 才补充上述条件,不能把缺失部分当成完整论证。

能拿来做什么

  • 在 Node 中做第一个工具型 Agent 时,可以先接只读工具,用模拟模型固定返回工具调用,验证各种停止状态,再接真实模型。退款示例里的写操作可以最后加入,并实现逐次审批。
  • 把工具参数校验和业务校验放进实际执行路径。即便模型传来了合法的 orderId,退款工具也应自行确认订单存在且状态允许操作,不能只依赖工具描述。
  • 接入真实费用统计后,按作者建议故意设置较低的 maxCostUsd,运行 10 个小任务,记录轮数与费用分布。如果某个任务达到中位数的 10 倍,先检查消息历史,再决定是否调整预算。
  • 选框架前列出当前确实需要的能力。只有几个工具、运行时间短,可以从小循环开始;必须恢复长任务或协调多个 Agent,就优先评估现成运行时,同时保留独立工具注册表,减少迁移范围。
Original source

本文由 AI 通读原文后重新编写,观点部分为站内判断,细节以原文为准,来源 dev.to · AI,原文版权归原作者所有。

阅读原文
上一篇
Cursor商城恶意扩展尝试植入SSH后门
下一篇
人工复核队列如何避免积压与敷衍审批

资讯速览

dev.to · AI · 2026-10-31 13:23

AI 评分 8.0 / 10

相关标签

#Agent#TypeScript#框架选型

继续阅读

  • RepoGeist 用本地 AI 追查代码历史
  • Cursor商城恶意扩展尝试植入SSH后门
  • 人工复核队列如何避免积压与敷衍审批
  • Figma评论删除后,AI记忆如何同步失效
  • OpenDsec 自托管沙箱支持运行态分叉