尧图精选

Harness 架构设计实战:用 PPAF 循环驱动 REPL 容器

🕒 发布时间:2026/10/1 20:03:12 📁 来源:尧图网络
1. 为什么要在本地搭一套 Harness 的 PPAF 循环如果你正在做 Agent 相关的工程大概率会遇到一个很具体的困境模型本身能跑工具也能调但整个执行链路是散的。感知靠拼字符串规划靠一次性 prompt行动直接裸调函数反思基本没有。跑一个 demo 没问题一旦任务超过三步就开始原地打转、重复调用、上下文爆炸。Harness 架构想解决的就是这件事。它把智能体的运行拆成两个正交的层语义层用 PPAF 循环定义“应该怎么走”——Perception 感知、Planning 规划、Action 行动、Feedback 反思机制层用 REPL 容器定义“按什么机制落地”——Read 读取、Eval 评估执行、Print 打印反馈、Loop 循环。前者是蓝图后者是轨道。我这次要落地的目标很明确在本地用一份 config.toml 骨架 一个容器启动配置把 PPAF 循环和 REPL 容器真正跑起来并且能亲眼看到一轮完整的“感知→规划→行动→反思”闭环。不是讲概念是能复制、能验证、能排错的运行环境。适合谁看需要搭建可交互执行环境的开发者尤其是已经在写 Agent 但觉得执行链路不可控、想引入容器化边界控制的人。你需要有基本的 Python 环境、Docker 基础以及一个能调用的模型 API。整篇文章的结构是先讲清楚 PPAF 和 REPL 在本地怎么对应到具体组件然后给出前置准备包括模型接入接着是可复制的 config.toml 和容器启动配置再演示一轮 PPAF 循环的验证步骤最后把常见的报错逐个排掉。全程围绕“可跟做”来写每一步都有命令和预期结果。先说清楚一个关键设计点避免后面看配置时懵REPL 容器里的 Eval 环节中央坐的是一个非确定性的 LLM。传统 REPL 的 Eval 是确定性求值器同样的表达式永远同样结果Harness REPL 的 Eval 要处理的是“怎么把不确定的推理收编进确定的执行”。所以容器必须有一道拦截器把 LLM 生成的意图先校验、再路由、再执行。这道拦截器就是我们本地要实现的 Call Interceptor也是整个配置里最需要认真对待的部分。2. 前置准备模型接入与 TaoToken 配置在写 config.toml 之前得先解决模型调用这一层。PPAF 循环里的 Planning 和 Feedback 两个环节都依赖 LLM本地跑的时候如果模型接入不稳定整个闭环会在 Eval 阶段反复超时排查起来会误以为是容器的问题。我这边用的是 TaoToken 做模型接入。它的定位是统一的模型调用入口兼容 OpenAI 风格的接口对本地 Harness 这种需要频繁调用、需要稳定 base_url 的场景比较合适。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 。接入需要三件套Base URL、API Key、Model ID。这三者在后面的 config.toml 里会分别对应base_url、api_key、model三个字段缺一不可。很多人配置失败就是因为只填了 Key 没填 Base URL或者 Model ID 写成了展示名而不是调用名。获取 API Key 的路径是进控制台创建https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 创建完在 API Keys 页面复制https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。Key 只在创建时完整显示一次记得当场存到本地环境变量里不要硬编码进 config.toml 提交到仓库。Model ID 的确认建议直接在模型对话页面测一下https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。选一个你打算用于 Planning 的模型发一条简单消息确认能返回然后把它的调用名记下来。这一步别省我见过太多人卡在 Model ID 拼错上报错信息还特别隐晦。环境变量这样设置Linux/macOS 下export TAOTOKEN_BASE_URLhttps://taotoken.net/api export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_MODEL你的模型调用名Windows PowerShell$env:TAOTOKEN_BASE_URLhttps://taotoken.net/api $env:TAOTOKEN_API_KEYsk-你的key $env:TAOTOKEN_MODEL你的模型调用名设置完验证一下能不能通用 curl 发一个最小请求curl -s $TAOTOKEN_BASE_URL/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: $TAOTOKEN_MODEL, messages: [{role: user, content: reply with ok}] }预期返回里能看到choices数组第一条 message 的 content 是 ok 之类的短回复。如果这里就报 401先别往下走把 Key 和 Base URL 对齐了再说。这一步通了后面容器里的 Eval 环节才有稳定的模型后端。如果你打算长期跑编码类或 Agent 类任务调用量会比较大可以考虑 Coding Plan 这种面向持续编码场景的方案https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面有各语言的调用示例配置字段对不上时以文档为准。3. 可复制配置config.toml 骨架与容器启动这一节是全文的核心给出能直接复制的 config.toml 骨架和容器启动配置。设计上遵循一个原则PPAF 的四个环节在配置里各有明确归属REPL 的四个机制各有对应组件治理策略作为横切配置单独成段。先看目录结构建议这样组织harness-local/ ├── config.toml ├── docker-compose.yml ├── Dockerfile ├── repl/ │ ├── context_manager.py │ ├── call_interceptor.py │ ├── tool_executor.py │ └── feedback_assembler.py └── tools/ └── registry.tomlconfig.toml 骨架如下字段注释写清楚每个对应 PPAF 的哪个环节# Harness 本地运行配置 # PPAF 语义层 REPL 机制层 治理策略 [harness] name local-ppaf-repl max_loop_rounds 8 # Loop 环节最大循环轮次防死循环 token_budget 120000 # Loop 环节Token 预算超了强制收敛 converge_on task_done # 终止判据任务完成即收敛 [model] base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY # 从环境变量读不硬编码 model 你的模型调用名 timeout_seconds 60 max_retries 2 # Read 环节上下文管理器对应 PPAF 感知 [repl.read] context_window 32000 # 上下文预算超了触发裁剪 trim_strategy summary # 裁剪策略summary / truncate include_history true history_rounds 6 # Eval 环节调用拦截器 工具执行器对应 PPAF 规划 行动 [repl.eval] interceptor_enabled true # 拦截器开关生产环境必须 true validate_schema true # 校验 LLM 输出的工具调用结构 tool_timeout_seconds 30 max_parallel_tools 3 risk_levels [readonly, write, dangerous] block_dangerous true # 高危操作直接拦截 # Print 环节反馈汇编器对应 PPAF 反思 [repl.print] assemble_observation true max_observation_tokens 4000 # 观测结果裁剪上限 keep_error_code true # 保留错误码反思依赖它 # Loop 环节循环控制 记忆沉淀 [repl.loop] memory_enabled true memory_store ./memory/ppaf.jsonl stop_on_error_streak 3 # 连续 3 次同类错误则终止 # 治理策略横切 PPAF 各环节 [governance.boundary] # 造缰边界约束 allowed_tools [read_file, write_file, run_shell, http_get] output_schema strict [governance.schedule] # 驭马调度控制 exec_order sequential circuit_breaker true [governance.observe] # 相马评估观测 metrics_enabled true log_level info [governance.iterate] # 育马迭代优化 review_after_task true这份配置里[repl.read]到[repl.loop]四段严格对应 REPL 的四个机制[governance.*]四段对应四步法治理策略。PPAF 的语义层没有单独成段因为它是由这四个机制段共同承载的——感知落在 read规划行动落在 eval反思落在 print闭环迭代落在 loop。容器启动用 docker-compose把 REPL 容器和工具执行环境隔离开version: 3.9 services: repl-container: build: context: . dockerfile: Dockerfile container_name: harness-repl environment: - TAOTOKEN_API_KEY${TAOTOKEN_API_KEY} - TAOTOKEN_BASE_URL${TAOTOKEN_BASE_URL} - TAOTOKEN_MODEL${TAOTOKEN_MODEL} volumes: - ./config.toml:/app/config.toml:ro - ./memory:/app/memory - ./tools:/app/tools:ro ports: - 8765:8765 # 边界控制限制资源对应造缰 deploy: resources: limits: cpus: 2.0 memory: 2g # 生命周期退出即销毁无状态残留 restart: no stdin_open: true tty: trueDockerfile 保持精简只装运行依赖FROM python:3.11-slim WORKDIR /app RUN pip install --no-cache-dir \ tomli \ httpx \ pydantic COPY repl/ /app/repl/ COPY tools/ /app/tools/ CMD [python, -m, repl.main]启动命令docker compose up --build预期看到容器起来后监听 8765日志里打印REPL container ready, PPAF loop armed。如果卡在 build 阶段多半是 pip 源的问题换国内源重试即可。这里有个设计细节值得强调block_dangerous true和allowed_tools白名单是配套的。拦截器在 Eval 环节捕获 LLM 的意图后先查白名单再查风险等级两者都过才路由到工具执行器。这就是“非确定性意图进入确定性执行”的那道闸配置里少一个闭环就有越权风险。4. 验证一轮 PPAF 循环从感知到反思配置就绪后跑一轮完整的 PPAF 循环来验证。这一节给出可复现的步骤和每步的预期输出你照着做应该能看到同样的结果。先准备一个最小任务比如“读取 tools/registry.toml统计里面注册了几个工具把结果写进 memory/result.txt”。这个任务足够简单但完整覆盖了感知、规划、行动、反思四个环节。启动容器后通过 stdin 或 HTTP 接口投递任务。这里用 HTTP 方式演示容器内起了一个轻量接口curl -s -X POST http://localhost:8765/task \ -H Content-Type: application/json \ -d {task: 读取 tools/registry.toml统计工具数量写入 memory/result.txt}第一轮循环的日志会分四段打印对应 PPAF 四个环节。感知阶段Read日志[PPAF][Perception] context assembled - user_task: 读取 tools/registry.toml... - history_rounds: 0 - context_tokens: 412 - trim: no_trim_needed这一步上下文管理器把用户指令、历史、系统状态整合成结构化 Prompt。context_tokens是实际占用远低于context_window说明没触发裁剪。规划阶段Eval 前半日志[PPAF][Planning] intent captured by interceptor - tool: read_file - args: {path: tools/registry.toml} - risk: readonly - validation: passed拦截器捕获到 LLM 生成的工具调用意图校验通过风险等级 readonly放行。行动阶段Eval 后半日志[PPAF][Action] tool executed - tool: read_file - duration_ms: 12 - status: success - output_tokens: 186工具执行器在监控下完成调用返回文件内容。反思阶段Print日志[PPAF][Feedback] observation assembled - result: parsed 4 tools from registry - error_code: none - observation_tokens: 96 - injected_to_context: true反馈汇编器把执行结果组装成结构化观测注入上下文供下一轮规划使用。第一轮结束后Loop 判断任务未完成还没写文件触发第二轮。第二轮里 LLM 基于上一轮的观测规划出write_file调用拦截器校验风险等级为 write放行执行器写入文件反馈汇编器确认写入成功。第三轮 Loop 判断任务完成收敛退出。最终返回{ status: converged, rounds: 3, final_output: memory/result.txt written, 4 tools counted, token_used: 2841 }验证文件确实写入了cat memory/result.txt # 预期输出4同时检查记忆沉淀文件tail -n 3 memory/ppaf.jsonl应该能看到三轮循环的经验记录每行一条 JSON包含轮次、环节、耗时、结果。这就是 Loop 环节的记忆沉淀下一轮任务会读取它来优化规划。到这里一轮完整的 PPAF 循环就跑通了。你能观察到的最关键现象是LLM 的每一次意图都经过了拦截器每一次执行都有回执每一轮都有观测注入。非确定性的推理被收编进了确定性的执行轨道这正是 REPL 容器的价值。如果想让验证更充分可以故意投递一个会触发拦截的任务比如“删除 memory 目录下所有文件”。预期看到拦截器在 Eval 阶段直接 block[PPAF][Planning] intent captured by interceptor - tool: run_shell - args: {cmd: rm -rf memory/*} - risk: dangerous - validation: BLOCKED by block_dangerous policy任务不会进入行动阶段直接返回被拦截。这一步验证的是造缰边界约束是否真的生效。5. 本篇常见报错排查配置和验证过程中有几个报错出现频率特别高逐个对照排查。401 Unauthorized / invalid api key这是最常见的一个。原因通常是三件套没对齐Base URL 填成了官网首页而不是 API 端点或者 API Key 没从环境变量正确传入容器。先确认base_url是https://taotoken.net/api不是带路径的完整 URL。再确认容器内环境变量docker compose exec repl-container env | grep TAOTOKEN如果TAOTOKEN_API_KEY是空的说明 compose 文件里的${TAOTOKEN_API_KEY}没读到宿主机的变量。检查宿主机是否 export 了或者改用.env文件放在 compose 同级目录。local proxy failed / connection refused这个报错说明容器内发起的模型请求没出去。常见原因是容器网络配置问题或者 base_url 写成了localhost。容器里的 localhost 指向容器自己不是宿主机。如果你在宿主机上跑了本地转发容器里要用host.docker.internal。但更推荐直接用 TaoToken 的 API 端点避免本地转发这一层。reading choices of undefined这个报错出现在解析模型返回时说明返回体里没有choices字段。两种可能一是请求根本没成功返回的是错误对象但代码没检查状态码就直接读choices二是 Model ID 写错了服务端返回了非预期结构。先在宿主机用第 2 节的 curl 命令确认模型能正常返回再把 Model ID 原样复制进 config.toml。解析代码里加一层判断resp httpx.post(url, jsonpayload, headersheaders, timeout60) data resp.json() if choices not in data: raise RuntimeError(funexpected response: {data}) content data[choices][0][message][content]OAuth / token expired如果你用的是需要 OAuth 的接入方式报这个错说明 token 过期了。TaoToken 的 API Key 方式不涉及 OAuth 刷新直接用 Key 即可。如果你在别处混用了 OAuth 配置把认证方式统一成 Bearer Key。interceptor validation failed: schema mismatch拦截器校验 LLM 输出结构失败。这通常是因为模型返回的工具调用格式和你的 schema 对不上比如该返回 JSON 却返回了自然语言。解决方向有两个一是在 Planning 的 prompt 里强化格式约束明确要求输出 JSON二是把validate_schema暂时设为 false 观察原始输出定位是格式问题还是模型能力问题。生产环境不建议长期关掉校验。loop exceeded max_loop_rounds循环超过最大轮次还没收敛。先看 memory/ppaf.jsonl 里最近几轮是不是在重复同样的动作。如果是说明反思环节没起作用观测结果没被正确注入上下文。检查assemble_observation是否为 true以及max_observation_tokens是不是设得太小导致关键信息被裁掉了。如果任务本身确实复杂适当调大max_loop_rounds但更该做的是优化任务拆解。tool timeout / circuit breaker triggered工具执行超时触发熔断。检查tool_timeout_seconds是否合理以及被调用的工具本身是不是卡住了。熔断触发后 Loop 会终止当前任务这是预期行为不是 bug。如果某个工具经常超时把它从allowed_tools里暂时移除单独调试。排查时有个通用技巧把log_level调到debug容器会打印每一轮完整的上下文和观测能快速定位是哪个环节断了。定位完记得调回infodebug 日志量很大。6. 把闭环跑稳之后配置和验证都通了之后有几个实践上的点值得留意。第一config.toml里的token_budget和max_loop_rounds要配套调。预算给得大但轮次给得少任务会在预算没用完时被强制收敛反过来则可能在预算耗尽时被截断。建议先按任务复杂度估一个轮次再按每轮平均消耗乘一个安全系数定预算。第二记忆沉淀文件会持续增长长期跑要加轮转策略。可以在 Loop 环节加一个按大小切分的逻辑或者定期归档。记忆是育马迭代优化的输入但无限增长会拖慢读取。第三拦截器的风险分级要随工具集更新。新增工具时先在allowed_tools里注册再给它标风险等级。漏标的话默认按 dangerous 处理会被直接拦截表现为“工具明明注册了却调不动”。第四如果你要把这套环境接到更完整的编码工作流里模型调用这层可以继续用 TaoToken 的 Coding Plan接入文档里有针对持续编码场景的配置建议。本地 Harness 负责闭环管控模型接入负责推理供给两层分开维护出问题时定位更快。整套环境跑通后你手上就有了一个可交互、可观测、可复盘的 PPAF 执行容器。接下来无论是加工具、换模型、调治理策略都在这套骨架里改配置就行不用动核心循环逻辑。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →