前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
返回 AI 情报前线
All News · 全部资讯10189
  • Drex Max 为电脑操作智能体输出决策概率
  • 无人值守的 Claude 循环如何耗尽额度
  • Bedrock为Claude提供严格结构化输出
  • 错误码搜不准?给向量检索补上 BM25
  • 让 Agent 崩溃后续跑,并处理重复副作用
  • 用 TypeScript 搭建带人工审批的客服智能体
  • Copilot 模型策略支持 REST API 管理
  • Copilot多种使用场景启动模型弃用
  • 把框架说明随包交付,让编程智能体可用
  • Nx 恶意包如何借编程智能体窃取秘密
  • 用分层防护限制 Agent 越权与误操作
  • 在表格中用自然语言分析完整BigQuery数据
  • 亚马逊推 TorchNeuron,拓宽 PyTorch 芯片选择
  • OpenCode平台遭入侵,密钥被盗刷
  • Cloudflare如何将AI审查成本降68%
  • 将Copilot任务无缝接续到VS Code
  • Gemini 4 Argon 主打多 Agent 三维制作
  • 用关联 ID 串起 Agent 日志与异常
  • MiniMax新模型以半价提升编程表现
  • 用ADK消除语音代理调用工具时的冷场
  • 62行Bash拆解编程代理的运行机制
  • 从调用轨迹定位代理的延迟与费用问题
  • AI 迁移脚本删库:备份失败为何未阻断
  • DeepSeek 推出 Agent 搜索与网页抓取接口
  • Bedrock新增长视频结构化解析模型
  • 语音识别迁移 Trainium2 的优化实战
  • vLLM 推送式 KV 传输降低推理等待
  • vLLM 双向复用 KV 缓存,降低多轮首字延迟
  • 英伟达发布面向企业与个人电脑的开放模型
  • Agent 工具报错,先判断副作用再重试
  • 在常驻虚拟机上搭建持久化编程 Agent 团队
  • 用DGX Spark承载本地n8n AI工作流
  • 训练数据权限如何随流水线变换传播
  • 用 AI 在 PR 中自动补齐架构决策记录
  • 用故障注入检验 AI 补丁的测试有效性
  • MiniMax新模型主打百万上下文与低价
  • 前端 PR 审查如何分配给 AI 与人
  • OpenCode 托管密钥疑泄露,影响部分用户
  • MiniMax 新模型降低编程与 Agent 成本
  • OpenAI补强小众语言编程能力与评测
  • AI 接口上线必备:限流、重试与降级
  • Blender AI 插件如何闭环执行代码
  • 用 CLAUDE.md 统一团队 AI 编程规范
  • 用金丝雀标记检测大模型信息泄露
  • 从基础到部署的大模型学习路线
  • 六块 AI 电路板实测:规则通过仍可能出错
  • 用Cloudflare为浏览器扩展接入限额AI
  • 把编程 Agent 的项目决策写进文件与 CI
  • 99.7% 召回率背后:花朵计数的评测盲点
  • 用代码知识图谱辅助编程智能体检索
  • 英伟达开放 Run:ai GPU 编排平台源码
  • 已加载 51 / 10189
8.0
热点
AI SCORE
编程提效2026-10-20 01:42

62行Bash拆解编程代理的运行机制

dev.to · AI#编程代理#Bash#开源
Editor brief · 编辑速览

教程仅用Bash、curl和jq,从单次模型调用逐步构建聊天机器人与编程代理,并演示生成俄罗斯方块游戏。配套源码已公开,可连接支持工具调用的本地或远程模型。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

这篇文章将带你一步步构建一个极简 AI harness:从单次 LLM 调用,到聊天机器人,再到能开发俄罗斯方块这类游戏的编程 Agent。整个过程只用 Bash 脚本。

全部源代码都在 GitHub 的 antonum/bash-harness 仓库中。

你需要准备:

  • 能够访问 LLM。这里我通过 OpenRouter 调用 OpenAI 的 LLM。你也可以使用其他支持 tool calling 的现代 LLM,无论是本地运行还是远程调用。
  • LLM 提供商的 API_KEY。
  • 一台配有终端、bash、curl 和 jq 的电脑。

第 1 步:调用 LLM

首先,我们要能从 bash 调用 LLM。参见 01-LLM-Call.sh。

OPENROUTER_API_KEY="sk-or-v1-12345....."

curl https://openrouter.ai/api/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENROUTER_API_KEY" \
  -d '{
  "model": "openai/gpt-6-luna",
  "max_tokens": 1500,
  "messages": [
    {
      "role": "user",
      "content": "In one sentence, what is jq?"
    }
  ]
}'

直接在终端运行这段代码,应该就能得到一大段 JSON。LLM 的回答就藏在其中。

用 jq 解析输出后,读起来会轻松一些,只需通过管道把结果传给 jq .。

bash 01-LLM-Call.sh | jq .
{
  "id": "gen-1792430673-n7TeT6PuaQl4j2fmQhpj",
  "object": "chat.completion",
  "created": 1792430673,
  "model": "openai/gpt-6-luna",
  "provider": "OpenAI",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "jq is a lightweight command-line tool for querying, transforming, and formatting JSON data.",
        "refusal": null,
        "reasoning": null,
        "reasoning_details": [ ... ]
      }
    }
  ],
  "usage": {
    "prompt_tokens": 14,
    "completion_tokens": 21,
    "total_tokens": 35,
    ...
  }
}

如果只想从这段 JSON 中提取回答文本,也可以用 jq:

bash 01-LLM-Call.sh | jq '.choices[0].message.content'

"jq is a lightweight command-line tool for querying, transforming, and formatting JSON data."

到这里一切顺利。我们可以在 bash 脚本中用 curl 调用 LLM,LLM 返回 JSON,再用 jq 解析结果。模型刚刚为我们解释了 jq 是什么,接下来我们会大量使用它。

这里有一点很重要:每次 LLM 调用都完全“无状态”。LLM 不知道你之前问过什么,也不知道自己昨天或几秒前回答过什么。它始终只根据当前请求中的上下文和自身知识作答,除此之外没有别的信息。补充合适的上下文,也就是聊天记录,以及给 LLM 提供可执行的工具,比如读写文件,都是 harness 的职责。

接下来的两步,我们就来构建这个 harness。开始吧!

要把单次调用变成聊天,只需要做两件事:记住所说过的话,以及循环调用。参见 02-Chat.sh。

对话其实就是保存在 shell 变量中的一个 JSON 消息数组。我们不手写 JSON,而是让 jq 来构建:

MESSAGES='[]'

add_message() {
  MESSAGES=$(jq --arg role "$1" --arg content "$2" \
    '. + [{role: $role, content: $content}]' <<<"$MESSAGES")
}

把第 1 步中的 curl 调用放进一个函数,接收整个数组,并将它作为 messages 发送:

call_llm() {
  local body
  body=$(jq -n --arg model "$MODEL" --argjson messages "$1" \
    '{model: $model, max_tokens: 1500, messages: $messages}')

  curl -s https://openrouter.ai/api/v1/chat/completions \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $OPENROUTER_API_KEY" \
    -d "$body"
}

然后是循环:读取一行输入,以 user 身份追加到消息中,调用 LLM,以 assistant 身份追加回复,打印出来,再重复这个过程。

while true; do
  IFS= read -e -r -p "you> " input || break
  [ -z "$input" ] && break

  add_message user "$input"
  response=$(call_llm "$MESSAGES")
  reply=$(jq -r '.choices[0].message.content' <<<"$response")
  add_message assistant "$reply"

  printf 'llm> %s\n\n' "$reply"
done
$ bash 02-Chat.sh
Chat with openai/gpt-6-luna. Empty line or Ctrl-D to quit.
you> My name is Anton
llm> Nice to meet you, Anton! How can I help you today?

you> What is my name?
llm> Your name is Anton.

第二次调用前,MESSAGES 的内容如下。这就是模型看到的全部请求内容,没有其他东西:

[
  { "role": "user",      "content": "My name is Anton" },
  { "role": "assistant", "content": "Nice to meet you, Anton! How can I help you today?" },
  { "role": "user",      "content": "What is my name?" }
]

注意,LLM 之所以“记得”这个名字,只是因为我们在第二次调用时又发送了一遍完整对话。服务端没有记忆。harness 就是记忆。每一轮,数组都会变长;每一轮,我们都会把整个数组发送过去。

这就是一个用 40 行 bash 写成的聊天机器人。现在,让它干点实事。

第 3 步:给 LLM 配上工具

工具没什么神奇的。我们告诉 LLM:“这里有一个函数,你可以要求我调用它。”随后,LLM 不再用文本回答,而是回复:“请用这些参数运行这个函数。”我们执行函数,把结果发回去,LLM 再继续处理。参见 03-Harness.sh。

我们只给它一个工具,也是威力最大的那个:bash。

TOOLS='[{
  "type": "function",
  "function": {
    "name": "bash",
    "description": "Run a bash command on the user'"'"'s machine and return its stdout, stderr and exit code.",
    "parameters": {
      "type": "object",
      "properties": { "command": { "type": "string", "description": "The command to run." } },
      "required": ["command"]
    }
  }
}]'

这段 JSON 会作为 tools 字段,与 messages 一起放进每个请求。call_llm 增加一个字段,并改成一条管道:jq 构建请求,curl 通过 -d @- 从 stdin 读取。每次调用都会重新发送完整对话,而对话只会越来越长;macOS 上命令行大小的上限是 1 MB,因此请求体始终不作为命令行参数传递。

call_llm() {
  jq -n --arg model "$MODEL" --argjson messages "$1" --argjson tools "$TOOLS" \
    '{model: $model, max_tokens: 8000, messages: $messages, tools: $tools}' |
  curl -sS https://openrouter.ai/api/v1/chat/completions \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $OPENROUTER_API_KEY" \
    -d @-
}

现在,我们还需要追加三种不同结构的消息:用户文本、assistant 回复,以及工具执行结果。我们没有为此编写三个函数,而是让 add_message 接收 jq 参数和一个对象表达式,这样每个调用位置都能明确写出要追加的 JSON:

add_message() {
  MESSAGES=$(jq --argjson m "$(jq -nc "$@")" '. + [$m]' <<<"$MESSAGES")
}

add_message --arg c "$input" '{role: "user", content: $c}'
add_message --argjson m "$message" '$m'
add_message --arg id "$call_id" --rawfile c <(printf '%s' "$result") \
  '{role: "tool", tool_call_id: $id, content: $c}'

最后一次调用使用了 --rawfile 和进程替换,原因与 curl 使用 -d @- 一样:工具输出大文件时,不能让命令行超出大小限制。

另一个新增部分,就是执行命令,并捕获模型了解执行情况所需的全部信息:

run_tool() {
  local output status=0
  output=$(bash -c "$1" 2>&1) || status=$?
  printf '%s\n[exit code: %d]' "$output" "$status"
}

接下来是循环。之前的流程是:用户说点什么,LLM 回复,然后打印。有了工具后,还需要第二层内部循环:LLM 回复,如果回复中包含 tool_calls,就执行这些调用,把结果作为 tool 消息追加进去,再次调用 LLM。只有当回复中不再包含工具调用时,我们才打印回复,把控制权交还给用户。

add_message --arg c "$input" '{role: "user", content: $c}'

while true; do
  response=$(call_llm "$MESSAGES")
  message=$(jq -ce '.choices[0].message' <<<"$response") ||
    { printf 'ERROR: %s\n\n' "$(jq -r '.error.message // .' <<<"$response")"; break; }

  add_message --argjson m "$message" '$m'   # echo it back verbatim, tool_calls and all

  if ! jq -e '.tool_calls | length > 0' <<<"$message" >/dev/null; then
    printf 'llm> %s\n\n' "$(jq -r '.content' <<<"$message")"
    break
  fi

  while IFS= read -r call; do
    call_id=$(jq -r '.id' <<<"$call")
    command=$(jq -r '.function.arguments | fromjson | .command' <<<"$call")

    printf 'bash> %s\n' "$command"
    result=$(run_tool "$command")
    add_message --arg id "$call_id" --rawfile c <(printf '%s' "$result") \
      '{role: "tool", tool_call_id: $id, content: $c}'
  done < <(jq -c '.tool_calls[]' <<<"$message")
done

有两个细节需要了解。包含 tool_calls 的 assistant 消息必须按收到时的原样放回历史记录,因此我们追加原始 JSON,而不是重新构建它。另外,每个工具结果都带有它所回应的 tool_call_id,方便 API 将两者对应起来。

脚本还有两个上面没有展示的小功能。curl 等待模型响应时,一个后台子 shell 会每秒在同一行刷新一次 thinking Ns,响应到达后用 kill $! 停止它。另外,如果回复因 max_tokens 限制而被截断,参数字符串可能不完整,导致无法解析;脚本会捕获这种情况,并向模型返回错误,而不是直接崩溃。

这个内部循环就是整个“Agent”。其余的一切,包括 Claude Code、Codex、Cursor 等,都是在这个循环之上做了大量完善。

$ bash 03-Harness.sh
Agent with bash tool (openai/gpt-6-luna). Empty line or Ctrl-D to quit.
you> what's the weather in Toronto?
bash> curl -s 'wttr.in/Toronto?format=3'
      Toronto: ⛅️ +12°C
      [exit code: 0]
llm> It's currently partly cloudy and about 12°C in Toronto.

向第 2 步的聊天机器人提出同样的问题,它会告诉你,它无法访问实时数据。同一个模型,同一个问题。区别就在请求中的 tools 字段。模型看到有 bash 工具,推断出有 shell 就能访问网络,于是自行选择 curl 和天气服务,读取输出,再组织成回答。我们从未在脚本的任何地方提到 curl、wttr.in 或天气。这就是工具的意义:模型负责规划,harness 只负责执行模型提出的要求。

下面是这轮交互后的 MESSAGES 数组。五条消息,两次 LLM 调用。第一次调用返回了包含 tool_calls 的消息,第二次调用返回了最终文本:

[
  {
    "role": "system",
    "content": "You are a helpful assistant running on the user's machine. Use the bash tool ..."
  },
  {
    "role": "user",
    "content": "what's the weather in Toronto?"
  },
  {
    "role": "assistant",
    "content": null,
    "tool_calls": [
      {
        "id": "call_x7Kq2",
        "type": "function",
        "function": {
          "name": "bash",
          "arguments": "{\"command\": \"curl -s 'wttr.in/Toronto?format=3'\"}"
        }
      }
    ]
  },
  {
    "role": "tool",
    "tool_call_id": "call_x7Kq2",
    "content": "Toronto: ⛅️ +12°C\n[exit code: 0]"
  },
  {
    "role": "assistant",
    "content": "It's currently partly cloudy and about 12°C in Toronto."
  }
]

这里有几处值得留意。arguments 字段是嵌在 JSON 中的 JSON 字符串,所以脚本要用 fromjson 解析它。tool 消息回应的是某个特定的 tool_call_id。工具输出只是文本,模型会像你一样理解 [exit code: 0]。在任意 add_message 调用后面加上 jq . <<<"$MESSAGES" >&2,就能实时观察这个数组如何增长。

下面是之前承诺的压轴演示:

you> write a tetris game in python, make sure it runs
bash> python3 --version
      Python 3.13.2
      [exit code: 0]
bash> cat > tetris.py <<'EOF'
      #!/usr/bin/env python3
      """A small, dependency-free Tetris game. Run with: python3 tetris.py"""
      ...
bash> python3 -c "import ast; ast.parse(open('tetris.py').read())"
      [exit code: 0]
bash> python3 -c "import tkinter"
      ModuleNotFoundError: No module named '_tkinter'
      [exit code: 1]
bash> cat > tetris.py <<'EOF'
      ...
llm> Done. tetris.py is written. Tkinter wasn't available on this machine, so the game falls
     back to a curses version that runs in the terminal. Run it with: python3 tetris.py

看看刚才发生了什么。模型检查了 Python 版本,写入文件,验证代码能否解析,发现缺少 Tkinter,然后重写游戏,加入终端运行的备用方案。没有人告诉它要做这些。我们给了它一个工具和一个循环,规划则来自模型。

生成的 tetris.py 就在仓库里。它能运行。

提醒一句:这个脚本会使用你的权限执行模型要求的任何操作,不会询问。这是一个教程。请留意屏幕上滚动显示的命令,也不要在你在意的目录中运行它。

三个脚本,去掉注释和空行后,总共只有 62 行 bash:

  • 用 curl 调用 LLM,用 jq 读取回答。
  • 用一个 JSON 数组保存历史记录,在调用外面套一层循环。这就是聊天。
  • 加上工具定义、执行工具的函数,以及一个持续运行、直到模型不再请求工具的内部循环。这就是 Agent。

真正的编程 Agent 在此之上增加的东西,比如更多工具、权限、上下文管理、更友好的输入方式,都是围绕这个核心展开的工程工作。核心本身,一屏就能装下。

Claude Code 和 Codex 就像现代汽车:安全气囊、电动车窗、空调,一应俱全。我们在这里造出的,则是发动机、变速箱、底盘和四个轮子。配置简陋到了极点,但它确实能开。下面这些功能,花一个下午就能加上:

  • 将当前目录中的 AGENTS.md 加载到 system prompt,让 Agent 了解项目约定。
  • 输入历史:read -e 已经使用了 readline,因此只需在每次读取后执行一次 history -s "$input",就能用上下方向键翻阅输入。
  • 执行工具调用前请求批准。这样,上面的警告就能变成一项功能。
  • 限制工具输出大小,并为 run_tool 添加超时,避免一次 cat 大文件或一条卡住的命令毁掉整个会话。
  • 每轮结束后把 MESSAGES 保存到文件,启动时再加载。这就是会话恢复。
  • 更多工具:read_file、write_file、edit_file。每个工具都只需要一个 schema 和一个函数。

如果还想采取进一步措施,可以考虑屏蔽此人和/或举报滥用行为。

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
用ADK消除语音代理调用工具时的冷场
下一篇
从调用轨迹定位代理的延迟与费用问题