在 AI 编码助手领域,主流方案通常依赖于复杂的 Python 或 Rust 技术栈。然而 pu.sh 项目展示了一种极简主义路线 —— 仅用约 400 行 POSIX Shell 代码实现了一个功能完整的 LLM 驱动编码智能体。本文从工程实现角度深入分析其核心架构,为资源受限环境下的智能体部署提供参考。
一、极简架构:为什么选择 Shell
pu.sh 的核心设计理念是「零依赖」—— 不需要 npm、pip 或 Docker,仅需 curl、awk 和一个 API Key 即可运行。这种设计带来了显著优势:部署时仅需一条 curl 命令即可获取脚本,无环境配置成本,且代码量可控在单文件内。相较于 Python 生态中动辄数千行的代理框架(如 LangChain、AutoGen),400 行代码意味着更低的理解门槛和更强的可审计性。
从技术选型角度看,Shell 的优势恰好契合编码智能体的核心需求。Shell 天生擅长文件操作、进程管理和管道组合,这些都是智能体执行工具调用时最频繁的操作。更重要的是,Shell 的交互式特性天然适合实现 REPL 风格的对话循环 —— 用户输入任务、智能体执行、返回结果、等待下一轮输入。
二、核心循环:LLM 驱动的任务执行
pu.sh 的主循环遵循经典的「观察 - 推理 - 执行」模式。启动时,脚本首先加载环境变量(API Key、模型选择、上下文限制等),然后根据输入参数决定运行模式:单次任务模式、管道模式或交互模式。
while [ "$step" -lt "$MAX_STEPS" ]; do
step=$((step+1))
MSGS=$(trim_context "$MSGS")
_STATE=busy
# 调用 LLM API
resp=$(call_api "$MSGS")
# 解析响应
parse_response "$resp"
# 执行工具调用
if [ "$TY" = "T" ]; then
run_tool "$TN" "$TINP"
# 将结果追加到上下文
append "..."
else
# 直接返回文本响应
printf '%s\n' "$TX"
fi
done
这个循环中包含三个关键工程点:上下文管理(trim_context)、API 调用封装(call_api)和工具执行(run_tool)。默认配置下,最大步数为 100 步,单次输出 token 上限为 4096,这为大多数编码任务提供了足够的探索空间,同时避免无限循环风险。
三、JSON 解析:AWK 的妙用
由于不能依赖外部 JSON 库,pu.sh 使用纯 AWK 实现了完整的 JSON 解析能力。脚本中定义了 jp()(获取值)和 jb()(获取块)两个核心函数,分别用于提取 JSON 对象的特定字段和嵌套块。
jp(){
printf '%s' "$1" | awk -v k="$2" '...'
}
jp 函数处理三种 JSON 值格式:字符串(双引号包裹)、数组和对象。它实现了完整的转义字符处理,包括 Unicode 编码(\uXXXX)和换行符转义。jb 函数则用于定位并提取完整的 JSON 块,特别适用于提取函数调用参数这类嵌套结构。
这种手写解析器的做法虽然降低了代码可读性,但完全避免了 Python/Node.js 等运行时依赖,使得脚本可以在任何类 Unix 系统上即插即用。性能测试表明,对于典型的 API 响应(几千字节 JSON),AWK 解析耗时在毫秒级别,完全可接受。
四、工具系统:七种能力的实现
pu.sh 定义了七个核心工具,涵盖编码任务中最常见的操作:
bash — 执行任意 Shell 命令。实现上通过临时文件传递命令脚本,捕获 stdout 和 stderr,退出码作为执行状态返回。关键设计在于使用 $RUNSH(默认为 bash,不存在时回退到 sh)确保命令执行的兼容性。
read — 读取文件内容。支持 offset 和 limit 参数,允许分块读取大文件。超过 AGENT_READ_MAX(默认 1MB)的文件会触发错误,要求调用者指定读取范围。
write — 写入文件。自动创建父目录,保留原有文件的权限模式,使用原子性的临时文件 + 重命名操作确保写入安全性。
edit — 精确文本替换。要求提供完整的 oldText 和 newText,通过 AWK 实现单次匹配替换。如果匹配次数不为 1(即 0 次或多次),会返回明确错误而非静默修改,这是防止意外批量替换的安全设计。
grep / find / ls — 文件搜索三件套。默认排除 .git、node_modules 等常见无关目录,grep 限制输出前 100 行以控制上下文大小。
每个工具执行后,输出会被截断(AGENT_TOOL_TRUNC 默认 100KB)再返回给 LLM,防止过大输出污染对话上下文。这种截断策略结合分页读取能力,使得智能体可以处理任意规模的项目。
五、上下文管理:智能体的大脑
LLM 的上下文窗口是稀缺资源,pu.sh 实现了多层上下文管理策略以最大化利用:
自动压缩(Compaction):当上下文大小超过 AGENT_CONTEXT_LIMIT - AGENT_RESERVE 时,触发压缩流程。压缩分为两种模式:LLM summarization(调用模型生成摘要)和 local memory(本地生成关键信息卡片)。前者效果更好但消耗额外 token,后者作为兜底方案。
保留策略:最近的 AGENT_KEEP_RECENT(默认 80KB)内容会被完整保留,确保最新状态不丢失。压缩过程会提取关键信息:用户意图、已读 / 已修改文件列表、执行过的命令、错误信息和决策记录。
OpenAI 兼容性:对于 OpenAI 的 function calling 格式,脚本需要特殊处理配对的 function_call 和 function_call_output 消息,确保上下文压缩后消息结构仍合法。
上下文管理参数可通过环境变量调整:
| 参数 | 默认值 | 用途 |
|---|---|---|
| AGENT_CONTEXT_LIMIT | 400000 | 上下文总大小上限 |
| AGENT_RESERVE | 16000 | 保留给单次响应的空间 |
| AGENT_KEEP_RECENT | 80000 | 完整保留的最近内容 |
| AGENT_TOOL_TRUNC | 100000 | 单次工具输出截断阈值 |
六、错误处理与恢复机制
智能体执行过程中可能出现多种错误,pu.sh 实现了分级处理策略:
API 层错误:网络超时(120 秒)、认证失败、模型不存在、上下文溢出等。认证失败直接退出;模型不存在提示用户切换模型;上下文溢出则触发压缩重试(ctx_retry 标志)。
工具层错误:文件不存在、权限不足、命令执行失败等。工具执行会捕获退出码,错误信息格式化为 Error: ... 或 [exit: N] 返回给 LLM,模型据此决定重试策略。
中断处理:脚本注册了 INT 和 PIPE 信号处理器。收到 Ctrl+C 时,如果智能体正在执行工具调用(_STATE=busy),会递归 kill 整个子进程树(包括后台 spinner),确保没有僵尸进程残留。
断点恢复:通过 AGENT_HISTORY 环境变量配置会话持久化。中断后重新运行脚本,会自动加载历史上下文,实现断点续传。
七、生产级参数配置
将 pu.sh 投入实际使用时,建议根据场景调整以下参数:
开发调试:设置 AGENT_VERBOSE=1 开启详细日志,AGENT_DEBUG_API=/tmp/debug 将每次 API 调用输入输出保存到文件,AGENT_CONFIRM=1 启用工具执行确认(交互模式生效)。
成本控制:设置 AGENT_PRICE_IN_PER_MTOK 和 AGENT_PRICE_OUT_PER_MTOK(每百万 token 美元价格),脚本会在每次调用后累计并展示当前会话成本。使用 --cost 参数可直接查看 token 消耗统计。
安全加固:在共享环境中,考虑设置 AGENT_CONFIRM=1 强制确认危险操作;通过白名单文件(.pu-allowed-commands)限制可执行的命令范围。
性能优化:增加 AGENT_MAX_STEPS 应对复杂任务;调整 MAX_TOKENS(默认 4096)给模型更多输出空间;通过 AGENT_EFFORT 参数控制推理深度(OpenAI 支持 none/minimal/low/medium/high/xhigh,Claude 支持 low/medium/high/max)。
八、部署实践与局限
pu.sh 的极简设计适合以下场景:快速原型验证、CI/CD 辅助脚本、容器镜像中的轻量 agent、资源受限的嵌入式环境。对于生产级应用,需要注意几个局限:纯 Shell 缺少内置的代码解释器保护,恶意 prompt 可能导致数据泄露;AWK JSON 解析对畸形 JSON 的容错能力有限;缺乏流式输出支持,响应延迟等同于完整生成时间。
在实际部署中,建议通过 Docker 封装并限制网络访问范围,配合 AGENT_CONFIRM=1 和严格的环境变量控制,将风险控制在可接受范围内。400 行代码的透明性使得安全审计成本极低,这是复杂框架难以比拟的优势。
资料来源:
- pu.sh 官方网站与源码:https://pu.dev
- 原始实现脚本:https://pu.dev/pu.sh