去了一趟青岛和九寨

入秋以来的成都一改往年的秋高气爽,变得阴沉多雨。索性也到了适合出行的时间,跟随影视飓风《去了一趟》系列的步伐,去了一趟青岛和九寨。

先入为主的青岛应该是一个海滨城市,虽然未必充满阳光,但是好歹应该有海风和海浪的陪伴。然而第一天晚上打车出行就让我发现这个海边的城市其实是多山和崎岖的。青岛的地形起伏不平,街道蜿蜒曲折,倒是非常适合City Walk。跟友人在傍晚时分用脚步度量了几个浴场,在灯塔处,恰逢日落,红霞满天,一次可遇不可求的火烧云,铺满天边的云朵,映射在海面上,心满意足。就着海风,小酌畅饮,大口吃肉。也不知道我那晚的表达是否合适,但这也许就是今朝有酒今朝醉吧。

名声在外的崂山也开启了下半年的第一次登山徒步。青岛的山海相依,给人一种别样的城市风情。从仰口山脚拾阶而上,中途体力挣扎,登顶一望无垠的海面,似乎很难相信海拔只有300多米。我想,古人大抵是没有严格和绝对的海拔概念的,不然应该是很难根据这般海拔写出“东临碣石,以观沧海”这样雄浑的诗句的。

月底安排了一次跟家人的九寨之行。九寨沟的秋天是最美的季节,但是最近的天气阴雨连绵,我给自己的预期也一直是平常心。至于父母能否满意,就不要太工于心计了。第一晚住在沟门口,给小马驹充电的时候,毫无意外的迎来了夜雨伴随着一点惊雷。晚上听着窗外的淅淅沥沥,看着旁边的父母和二宝,内心却异常的宁静和满足。游览路线稍微做了调整,从景区第一个景点向上走,一种渐入佳境的感觉油然而生。同时也几乎避开了所有主流的路线和游客,在沟内的林间小道上,偶尔会遇到几位游客,更多的时候是与大自然独处。第二晚住在沟内,景区关门以后,沟内基本只有本地村民,以及偶尔一两个来酒店串门而心照不宣的游客。夜晚的沟内,静谧而神秘,仿佛进入了一个与世隔绝的世界。清晨醒来,阳光灿烂,感恩感谢,一切都是最好的安排。

写下以上文字的时候,已经到了广州。我其实很想知道妞儿这次计划为什么总感觉是“反向跑毒”,但是也许这就是这两年旅行之于自己的意义吧:有些时间是分享给家人的,有些时间是分享给朋友的,有些时间是分享给自己的。而分享给自己的时间,往往难得,却也终结会给自己兑现。

快速搭建一个轻量级Agent平台

前几天在跟同行探讨多租户 Agent 平台的架构设计时,发现一个很有意思的现象:很多人在面对 LLM 应用编排时,依然带着传统的框架执念。一上来就试图引入各种厚重的生态,比如完整的 Spring AI 体系或者 LangChain 的各类高级组件(比如 @AiService 和 @Tool)。

然而,当业务场景变成“千级 Agent、动态配置、毫秒级按需装载”的时候,这些在单体应用中大放异彩的框架,往往成了阻碍工程扩展性和系统稳定性的累赘。

如果你最近有去扒一扒 Anthropic 泄露的 Claude Code CLI 源码,或者关注过 Pi 这种极简 Agent,你会发现那些真正顶级的工程团队在 Agent 架构上都在做减法:抛弃臃肿的编排黑盒,拥抱最纯粹的控制流。

本文将以 Java 服务端为背景,记录如何剥离 AI 框架的“语法糖”,仅仅依靠 LangChain4j-Core 的底层原语,手搓一个支持 MCP (Model Context Protocol)、纯数据驱动且能支撑高并发的动态 Agent Loop 引擎。

一、 阶段一:千级 Agent 平台的工程痛点

在构建 SaaS 级的 Agent 平台(或者说一个 Local Token Hub)时,系统通常要承载海量且配置各异的虚拟助手。每次请求,网关都要根据请求头的 agent_id,将特定的系统指令(Skill)、专属工具(Tool)组装好并喂给模型。

如果此时采用高层框架的静态绑定模式,会暴露出几个工程上的致命伤:

  1. 静态工具绑定与动态调度的错位: 传统框架要求你将工具写死为 Java 类的方法。但在平台场景中,Tool 往往只是存放在数据库里的 OpenAPI 描述或是 JSON Schema。我们需要的是“元数据先行,按需加载”,而不是预编译。
  2. 资源与 Context 开销: 为几千个 Agent 初始化几千个包含 Memory Provider、Chat Memory、Agent Executor 的重量级常驻对象,服务器的堆内存很快就会被撑爆。
  3. 黑盒化与控制流丧失: 当你需要深度接入 MCP,或是需要对每次 Tool Call 进行细粒度的 Token 计费、权限沙箱校验时,被框架完全接管的 ReAct Loop 会让你寸步难行。

二、 阶段二:大道至简的 Agent Loop

“An autonomous agent is just an LLM + tools + a loop.”

这句话堪称 Agent 工程化里的一句箴言。剥离掉所有包装,Agent 的执行层本质上就是一个 While 循环。

我们要做的,是在每次 API 请求到达时,执行以下“三位一体”的动态组装,并送入循环:
* Skills (技能):根据 agent_id 提取 System Prompt(比如从 SKILL.md 中解析),作为上下文基调。
* Tools (本地工具):将数据库里配置的 API 描述转化为 LLM 能懂的 JSON Schema。
* MCP (模型上下文协议):连接外部数据源,将其提供的能力动态拉取并合并到工具列表中。

三、 核心实现:基于原语的“微内核”架构

作为一个开发者,我们要实现的是一个“微内核”的调度引擎。这里我们只引入 langchain4j-core 的底层原语,借用它抹平各家大模型 API 差异的能力,但控制流完全由我们自己掌握。

1. 依赖约束

我们不需要庞大的依赖树,只需要核心层即可:

<dependency>
<groupid>dev.langchain4j</groupid>
<artifactid>langchain4j-core</artifactid>
<version>0.35.0</version>
</dependency>
<dependency>
<groupid>dev.langchain4j</groupid>
<artifactid>langchain4j-open-ai</artifactid>
<version>0.35.0</version>
</dependency>

2. 动态 Agent Loop 引擎

以下代码展示了如何实现一个无状态、纯数据驱动的微型引擎:

import dev.langchain4j.data.message.*;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.agent.tool.ToolExecutionRequest;
import dev.langchain4j.model.output.Response;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.ArrayList;
import java.util.List;

public class DynamicAgentLoopExecutor {

// 全局复用无状态的 LLM Client
private final ChatLanguageModel chatModel;

public DynamicAgentLoopExecutor(String apiKey) {
this.chatModel = OpenAiChatModel.builder()
.apiKey(apiKey)
.modelName("gpt-4o")
.temperature(0.3)
.build();
}

/**
* 核心 Agent Loop (网关每次请求的无状态入口)
*/
public String runAgent(String agentId, String userMessage) throws Exception {

// ==========================================
// 1. 渐进式发现与 Context 构造
// ==========================================
// 从存储加载当前 Agent 的 Skill (System Prompt)
String systemPrompt = loadSystemPromptFromDb(agentId);

// 动态构造 Tool Schema,不依赖任何 @Tool 注解
List<toolspecification> dynamicTools = new ArrayList<>();
dynamicTools.add(ToolSpecification.builder()
.name("get_weather")
.description("获取指定城市的天气")
.addParameter("city", dev.langchain4j.agent.tool.JsonSchemaProperty.STRING)
.build());</toolspecification>

// 假如有 MCP 接入,这里直接调用 MCP Client 获取远端工具列表并 append 进来
// dynamicTools.addAll(mcpClient.listTools());

List<chatmessage> chatHistory = new ArrayList<>();
chatHistory.add(SystemMessage.from(systemPrompt));
chatHistory.add(UserMessage.from(userMessage));</chatmessage>

// ==========================================
// 2. 意图匹配与 ReAct 循环执行
// ==========================================
int maxIterations = 10; // Token Budget 与死循环熔断
while (maxIterations-- > 0) {

// 提交 LLM 推理
Response<aimessage> response = chatModel.generate(chatHistory, dynamicTools);
AiMessage aiMessage = response.content();
chatHistory.add(aiMessage);</aimessage>

// 如果模型未发起 Tool Call,直接完成文本输出
if (!aiMessage.hasToolExecutionRequests()) {
return aiMessage.text();
}

// 拦截到 Tool Call,进入执行侧的分发
for (ToolExecutionRequest toolRequest : aiMessage.toolExecutionRequests()) {
String toolName = toolRequest.name();
String toolArgsJson = toolRequest.arguments();
String toolResult = "";

try {
// 工程分化点:本地沙箱调用 vs 远程 MCP 调用
if (isLocalTool(toolName)) {
toolResult = executeLocalTool(toolName, toolArgsJson);
} else {
// toolResult = mcpClient.callTool(toolName, toolArgsJson);
toolResult = "Mock MCP Result";
}
} catch (Exception e) {
toolResult = "Error: " + e.getMessage();
}

// 将执行结果(Observation)封装追加,进入下一轮迭代
chatHistory.add(ToolExecutionResultMessage.from(
toolRequest.id(), toolName, toolResult
));
}
}

throw new RuntimeException("Agent iteration limit exceeded");
}

// ... 省略 mock 的辅助方法 ...
}

这段百行代码的优势在于:它是绝对无状态(Stateless)的。一千个 Agent 的配置只是存放在数据库里的静态文本,在调用时才进行内存 Context 构造,用完随 GC 销毁,非常契合 Serverless 和高并发网关的诉求。

四、 延伸思考:MCP、Skill 与“嵌入派”的胜利

在研究这段手搓的 Agent Loop 时,很容易让人联想到 Anthropic 推出的 MCP 标准。

对于多租户平台来说,MCP 就是一个面向 LLM 的微服务架构。在上面的代码逻辑中,LLM 其实并不关心 dynamicTools 中的某个工具是在当前宿主机执行的原生代码,还是通过网络请求转发到某台 MCP Server 上的。在引擎的视角,MCP 仅仅是提供了一堆动态的 ToolSpecification。

这再次印证了之前关于“大模型嵌入派”的观点:将 LLM 作为决策中枢(Router),自下而上地将其嵌入到可控的系统架构中。

太阳底下没有新鲜事,以前微服务架构下积累的限流、熔断、调用链追踪经验,在这个微内核 Agent 架构下依然有着极高的借鉴意义。你可以在上面的 While 循环中,轻易地插入 Token Budget 检查、权限拦截(沙箱控制)和分布式日志。

五、 结语

有时候,过于 FOMO(Fear of Missing Out)的心态会让我们在面对技术演进时,盲目去追求一些看似大而全的权威框架,这反而会损失工程决策的质量。

把 LLM 当成 CPU 内核,把 Prompt/Skill 当作指令集,把 Tool 和 MCP 当成系统调用,你会发现,设计一个高可用的 Agent 框架,跟一门编程语言最终要实现如何自举的设计哲学几乎完全一致。

面对千级 Agent 的工程挑战,扔掉那些过度封装的框架,回归到最基础的 Agent Loop。在确定性的架构上去嵌入大模型的能力,不断将其沉淀为系统的稳定模块,这或许才是我们面对这一轮技术浪潮时,最不该焦虑且最踏实的演进路线。

印象中,这已经是这两年以来,自己第4次相对正式的设计和实现特定业务场景下Agent平台了。虽然每次都会有一些不同的业务背景和技术栈,但是问题的本质一直都没有改变。而有显著的改变的是,通过vibe coding,我们可以非常快速的把整个平台扶上线。

上周刚刚发布的GPT-5.6出来以后,很多开发者发出了”是时候把gstack, superpowers卸载了“的感慨。虽然开发者社区总是喜欢短期内高度阶段技术进步的影响,模型的嵌入和折叠外部能力的速度未必如他们口中呼喊的那么快,但从工程角度来看,在这个不断萃取和沉淀的过程中,随着螺旋上升不断调整和适应新的底座和新的能力,才是我们在这个时代最应该关注的事情。

如何在 macOS 优雅地为 Codex CLI 与 App 设置代理

在国内网络环境或公司内网使用 OpenAI 推出的 Codex 工具时,网络连接问题经常是大家遇到最大的绊脚石。而且O家的软件版本质量吧,真的是一言难尽,代理的设置并不是完全前向兼容。

之前不想浪费时间,都是直接开TUN模式,但是这个模式对全局网络影响比较大,而且有一定的系统性能影响。忍无可忍之下,做了一下 Codex CLI 和 App的单独设置。

本文总结了在 macOS 下为 Codex CLI 和 Codex 桌面端 App 完美配置本地代理的实战经验,希望能让大家少浪费时间在网络连接问题上。

💡 提示: 下文均以本地 HTTP/HTTPS 代理端口 7897 为例。请在实际操作中将 7897 替换为你自己的代理软件端口(如 Clash 默认的 7890)。


1. Codex CLI 的代理设置:破除配置误区

很多人以为在 ~/.codex/config.toml 中配置 [features.network_proxy] 就可以让 CLI 连上 OpenAI,这其实是个误区。那个配置仅对 Codex 执行代码的“沙盒环境”有效,无法解决 Codex 本身连接 OpenAI API 的问题。

正确的解法: 依赖系统环境变量。

如果你想让终端里的所有命令或仅限 codex 命令走代理,最好的方式是在你的 shell 配置文件(如 ~/.zshrc)中设置:

打开终端执行 nano ~/.zshrc,将以下代码加入文件:

# 方案 A:仅为 codex 命令设置别名(推荐,不污染全局环境)
alias codex='https_proxy="http://127.0.0.1:7897" http_proxy="http://127.0.0.1:7897" codex'

# 方案 B:如果希望当前终端默认全局走代理,直接 export
# export http_proxy="http://127.0.0.1:7897"
# export https_proxy="http://127.0.0.1:7897"

保存后,执行 source ~/.zshrc 生效。从此在终端敲 codex 就能顺畅通信了。


2. Codex 桌面端 App 的代理设置:绕过 Electron 的坑

当你满心欢喜配置好环境变量,打开 Codex 桌面应用时,很可能依然会在日志里看到类似这样的报错:

ERROR [Statsig] A networking error occurred... Error: Timeout of 10000ms expired.
[electron-message-handler] error while bootstrapping post-login client

原因分析:
Codex App 是基于 Electron(Chromium 内核)开发的。常规的环境变量对它的 Node.js 后端进程有效,但其前端界面(Renderer 进程)会忽略环境变量,导致页面请求一直处于直连状态而超时。

优雅解法:一键启动别名(Alias)

我们需要同时为后台进程提供环境变量,并为前端内核传入 Chromium 专属启动参数 --proxy-server。为了不阻塞终端,我们可以用 nohup 让它在后台静默运行。

编辑 ~/.zshrc,在末尾追加如下配置:

# 完美启动 Codex App 的快捷命令
alias codex-app='http_proxy="http://127.0.0.1:7897" https_proxy="http://127.0.0.1:7897" nohup /Applications/Codex.app/Contents/MacOS/Codex --proxy-server="http://127.0.0.1:7897" > /dev/null 2>&1 &'

使配置生效并使用:

  1. 刷新配置:
source ~/.zshrc
  1. 杀掉之前卡住的旧进程(非常重要,否则新参数可能不生效):
pkill -f Codex
  1. 在终端中输入自定义的别名命令:
codex-app

效果:
回车后,终端立即返回空闲状态(你可以直接关掉终端)。同时,Codex App 会秒开,无论是界面的 A/B 测试请求、登录流,还是后续的对话,都能完美通过本地代理发出,不再转圈超时!


总结与备选方案

  • CLI 工具认准 http_proxy / https_proxy 环境变量。不需要乱加 all_proxy 如果你只有 HTTP 节点。
  • Electron 桌面端不仅需要环境变量兜底,更需要传递 --proxy-server 启动参数。
  • 备选大招:如果你觉得配代码太麻烦,或者上述方法因为企业证书抓包拦截依然报错,最一劳永逸的方法是:直接在你的代理客户端中开启 TUN 模式(虚拟网卡接管)。TUN 模式会在系统底层强制劫持所有的网络请求,无需对应用做任何额外配置。