← 返回主页 PROJECT 02 / 02

PROJECT 02

PaiCLI · Java Agent CLI

一个跑在终端里的 Java AI Agent:你提需求,模型自己决定读哪个文件、 搜什么关键字、执行什么命令,看结果再决定下一步,直到任务完成 —— 整个过程可审批、可回滚、可审计。

角色
独立开发
周期
2026.04 — 2026.08
项目重点
Agent 循环 · 工具协议 · 上下文工程 · 安全边界

01

为什么做它

市面上的 AI 编程助手,很多还停在「把代码贴进对话框」这一步。但真实工程里, 模型需要的是能自己去看:读文件、跑命令、看报错, 再根据结果调整下一步。

所以我实现了一个本地可执行的 Agent 运行时,把工具调用、上下文管理、 安全边界都做成可审计的东西 —— 而不是一个黑盒对话框。

和我另一个项目(企业级 RAG 知识库)正好互补:那个偏应用层的后端工程, 这个偏底层的 Agent 架构。

02

核心机制

ReAct 主循环与工具编排

「思考 → 工具调用 → 结果回灌 → 再推理」的自主循环, 退出由模型自决而不是固定轮数; 多工具并行执行(并发上限 4),通过下标对齐保证结果严格按原始请求顺序回灌, 满足 tool_call_id 配对协议; 内置死循环检测(工具名 + 参数签名连续 3 轮一致即中止)与 50 轮硬上限双重兜底。

上下文工程与自动压缩

LLM 是无状态的,每轮要重发全量历史。这里实现了按模型窗口派生的动态压缩阈值, 自动保留最近 3 个 user 轮次,并强制把切分点落在 user 消息边界 —— 避免切断 tool_call / tool_result 的配对。 摘要生成失败时放弃压缩,保证历史完整性。

MCP 协议与工具动态扩展

支持 stdio 与 Streamable HTTP 两种传输,工具以 mcp__{server}__{tool} 的形式动态注册,响应 tools/list_changed 通知热更新; 环境变量延迟到单个 server 启动时展开,实现故障隔离 —— 一个 MCP server 起不来,不阻塞主流程。

三层安全防线

HITL 人工审批 → PathGuard 路径围栏 → CommandGuard 命令黑名单纵深防御。 审批支持「全部放行 / 拒绝 / 跳过 / 修改参数后执行」; 路径校验基于 toRealPath() 解析真实路径,防符号链接逃逸; 危险操作写审计日志并对 token / password 等敏感参数脱敏。

Side-Git 快照与回滚

基于 JGit 建立独立的 GIT_DIR 快照,不污染项目的 .git, 支持按轮次回滚 —— 模型改坏代码时能退回上一步。

多模式执行

除默认 ReAct 外,还有 Plan-and-Execute (生成计划 → DAG 依赖建模 → DFS 环检测 → 拓扑顺序并行调度) 与 Multi-Agent(规划 / 执行 / 审查三角色,仅执行者持有工具权限)。 三条路径共享同一套工具注册表与记忆模块。

多模型适配

基于统一接口 + 抽象模板基类接入 7 个模型 provider,并针对性处理能力差异: thinking 模式要随历史回带 reasoning_content、 部分网络下长流会被网关重置需要强制 HTTP/1.1、 不支持图片的 provider 自动降级为文本提示。

03

我实现的核心模块

1. Agent 主循环与工具编排引擎

负责三件事:把模型的工具调用决策翻译成真实的本地执行、把执行结果按协议回灌给模型、 以及在模型"卡住"时兜底。并行工具的执行结果必须按原始调用顺序回填, 否则 tool_call_id 配不上,模型会收到错乱的历史。

2. 上下文与记忆体系

按模型窗口比例分配 Token 预算,设计三类记忆(对话 / 事实 / 工具结果)的注入与淘汰策略; 压缩时强制在 user 消息边界切分,宁可少压缩一点,也不切断工具调用配对。

3. MCP 协议接入

实现 stdio 子进程与 Streamable HTTP 两种传输的客户端,工具动态注册与列表热更新; 清洗工具 schema 里的嵌套引用与超长描述,降低模型调用失败率。

4. 安全防线与快照回滚

三层拦截按 HITL → PathGuard → CommandGuard 顺序执行, 用户无法批准被策略拒绝的请求;基于 JGit 的快照独立于项目仓库,支持按轮次回退。

04

几个关键的设计取舍

为什么上下文压缩的切分点必须落在 user 边界?
如果从任意位置切,很容易把一次 tool_call 和它的 tool_result 切到两半,模型收到一个「有调用没有结果」的历史,就会开始乱猜。这类问题不会报错, 只会让回答质量变差 —— 最难排查的那种。
为什么不直接用线程池做异步?
线程池做不到持久化:服务一重启,没处理完的任务就丢了。而在 Agent 场景里, 一次任务可能已经花了不少 token,丢掉等于白花钱。
为什么 PathGuard 要 toRealPath()?
只校验字符串前缀是能被绕过的:项目里放一个指向 /etc 的符号链接, 路径字符串看起来还在项目内,实际已经跑出去了。
为什么工具的并发上限是 4?
再高收益很小,但本地文件读写和命令执行会互相干扰,出问题的概率明显上升。 保守一点,换来的是结果可复现。

05

技术栈

Java 17MavenOkHttp (SSE 流式) JacksonJLine 4SQLite JGitJavaParserJsoup MCP 协议Ollama Embedding