1.1-main.cpp入口解剖

1.1-main.cpp入口解剖

源码仓库:

上一篇把三条命令拆开,最后落到一句建议:读代码从 src/main.cpp 开始。这篇就顺着这句话走进去。src/main.cpp 有 607 行,但里面几乎没有算法,全部内容是把配置、模型、运行时、工具、A2A 服务端和 Web 网关按正确的顺序创建出来,再用一组回调接在一起。这篇要回答三个问题:创建顺序为什么是这样、模块之间的依赖指向哪、每个 lambda 到底接住了哪条请求路径。

客户端这边先交代一句:项目的主线是终端 coding agent (de / cli/),它通过 WebGateway 的 HTTP/SSE 接口工作;web/node_frontend 那个浏览器前端只是当初用来验证链路的可选页面,不是重点。所以下面会把「coding agent 走的那条流式路径」当主线讲,浏览器入口只在必要时提一句。


1. 问题背景:入口文件为什么值得单独读

C++ 没有 Spring 那样现成的依赖注入容器,一个服务的「接线」是手写的。写的人不觉得难,读的人却常常卡住:变量太多、顺序有讲究、某些失败要直接退出,某些失败却可以降级。如果只看 AgentRuntime 或 DeepSeekClient 的实现,很容易漏掉它们是怎么被创建、被谁持有的。

main.cpp 就是回答这些问题的唯一位置。它实际上承担了四件事:

  • 解析启动参数:环境变量与命令行的优先级、默认端口和默认 db 路径。
  • 装载五段配置:DeepSeek、日志、MCP、压缩、项目指令,每段缺省行为不同。
  • 构造并注入组件 :谁先创建、谁持有谁、哪些是栈对象、哪些是 shared_ptr。
  • 注册回调 :把 A2A 的 SendMessage 和 coding agent 走的流式接口接到同一个 AgentRuntime。

难点不在某一行的写法,而在这四件事交织时的取舍。比如「DeepSeek 配置缺失就直接退出,MCP 加载失败却继续跑」,这类决定只写在 main.cpp 里,不读它就无从知道。另外,随着会话管理、压缩、项目指令这些功能加进来,入口文件已经积累了十来个回调,共 607 行,这也是一种需要正视的工程现状。


2. 整体结构:main() 的七个阶段

main() 可以按执行顺序切成七个阶段,前一阶段的产物是后一阶段的输入:

flowchart TD A[&#34;ParseOptions<br/>环境变量 + argv&#34;] --> B[&#34;装载配置<br/>DeepSeek 缺失直接退出&#34;] B --> C[&#34;ConfigureDefaultLogger<br/>先让日志可用&#34;] C --> D[&#34;创建共享组件<br/>TaskStore / SessionMetaStore / DeepSeekClient&#34;] D --> E[&#34;构造 AgentRuntime<br/>注入工具 / MCP 桥 / 指令加载器 / 压缩&#34;] E --> F[&#34;AgentServer + SetMessageHandler<br/>接同步 A2A 路径&#34;] F --> G[&#34;WebGateway + 一批 Set*Handler<br/>接 coding agent 的流式路径&#34;] G --> H[&#34;WebGateway.Start / AgentHttpServer.Start&#34;] H --> I[&#34;注册信号 + 主线程 100ms 轮询&#34;] I --> J[&#34;Stop 两个服务,进程退出&#34;]

有两个细节值得先点出来:

  1. 配置在前,日志紧随其后。因为后面每一段初始化都可能写日志,日志系统必须先在配置阶段末期就绪。
  2. 两个网络服务最后才启动。所有回调在主线程启动服务之前就注册完毕,因此请求进来时依赖已经全部就位,不需要为「服务已监听但回调还没设」做额外同步。

下面按阶段拆开。


3. 源码精读

3.1 选项解析:环境变量兜底,命令行覆盖

启动参数分成两层。AppOptions 先给一组默认值,ParseOptions() 先用环境变量覆盖默认值,再用命令行参数覆盖环境变量:

ini 复制代码
 struct AppOptions {
     int port = 18080;
     int web_port = 18081;
     std::string config_path = "conf/conf.ini";
     std::string db_path = "build/db/aiagent_tasks.db";
     std::string mcp_binary;
     // ... 日志、MCP 插件目录等
 };
 ​
 AppOptions ParseOptions(int argc, char** argv) {
     AppOptions options;
     if (const char* value = std::getenv("AIAGENT_PORT")) {
         options.port = std::stoi(value);
     }
     // ... AIAGENT_WEB_PORT / AIAGENT_CONFIG / AIAGENT_DB 同理
     for (int i = 1; i < argc; ++i) {
         const std::string arg = argv[i];
         if (arg == "--config" && i + 1 < argc) {
             options.config_path = argv[++i];
         }
         // ... --db / --port / --web-port / --log-file 等
         else {
             options.port = std::stoi(arg);  // 兼容直接传端口的历史用法
         }
     }
     return options;
 }

它解决的问题是同一份二进制在不同环境下改端口/路径 :容器里用 AIAGENT_PORT 注入,本地调试用 --port 覆盖。最后一个 else 分支把无法识别的裸参数当成端口,这是早期版本的调用方式,保留下来避免破坏已有脚本。

代价是这里没有 --help,也没有对 std::stoi 失败的保护------参数不是合法整数时会抛异常,被最外层的 catch 接到,打印一条致命日志后退出。对内部工具够用,但如果要做成对外分发的命令行程序,这里迟早要补参数校验和帮助信息。

3.2 五段配置:哪些必须、哪些可缺省

配置从同一个 conf.ini 分出五段,由五个独立的 Load*ConfigFile() 装载。它们的严格程度不一样:

INI 段 装载函数 缺失/失败时的行为
[deepseek] LoadDeepSeekConfigFile 直接打印 配置错误 并 return 1
[logging] LoadLogConfigFile 用默认值,之后可被 --log-* 覆盖
[mcp] LoadMCPConfigFile 没写就尝试自动发现 vendor 下的本地构建
[compaction] LoadCompactionConfigFile 用默认值(enabled=true,窗口 64000)
[instructions] LoadInstructionsConfigFile 解析失败会打印 配置错误 并退出(无该段时用默认值)

只有 DeepSeek 是硬门槛:

arduino 复制代码
 const auto config = aiagent::LoadDeepSeekConfigFile(options.config_path, &config_error);
 if (!config.has_value()) {
     std::cerr << "配置错误: " << config_error << '\n';
     return 1;
 }

原因很直接:没有 API Key 和模型名,整个服务没有任何可用的对话能力,继续跑只会让每个请求都失败,不如启动时就报清楚。.env / DEEPSEEK_API_KEY 的回退逻辑封装在 LoadDeepSeekConfigFile 里,入口这里不用关心。

日志的装载顺序是:先读 [logging],再用命令行参数逐个覆盖,最后一次性配置全局 Logger:

ini 复制代码
 aiagent::LogConfig log_config;
 if (const auto parsed_log = aiagent::LoadLogConfigFile(options.config_path, &log_error)) {
     log_config = *parsed_log;
 }
 if (!options.log_file.empty())  log_config.file  = options.log_file;
 if (!options.log_level.empty()) log_config.level = options.log_level;
 if (options.log_console)        log_config.console = true;
 aiagent::ConfigureDefaultLogger(log_config.level, log_config.file, log_config.console,
                                 log_config.max_size_bytes, log_config.max_files);

这样后续所有模块(DB、模型、运行时、网关)产生的日志都能落到同一个 Logger 上,request id 也才能从 Web 一路串到 DeepSeek。

3.3 组件构造:栈对象与 shared_ptr 的分工

这一段是 main() 的核心。先把持久化层建好,再建模型客户端,最后才是 AgentRuntime:

php 复制代码
 // 1. 持久化:任务库 + 会话元数据(同目录的 JSON)
 auto task_store = std::make_shared<a2a::server::SqliteTaskStore>(db_path.string());
 std::filesystem::path meta_path = db_path;
 meta_path.replace_filename("aiagent_sessions.json");
 aiagent::agent::SessionMetaStore session_meta(meta_path.string());
 ​
 // 2. 模型客户端
 auto model = std::make_shared<aiagent::llm::DeepSeekClient>(*config);
 ​
 // 3. 运行时:system prompt 直接写在这里,可注入工具/压缩/指令
 aiagent::agent::AgentRuntime runtime(model, coding_system_prompt);
 runtime.SetFallbackWorkingDirectory(std::filesystem::current_path().string());
 ​
 // 4. 工具:内置 coding 工具先加,MCP 桥稍后按需追加
 auto tools = std::make_shared<aiagent::tools::CompositeToolProvider>();
 tools->Add(std::make_shared<aiagent::tools::LocalCodingTools>(
         std::filesystem::current_path().string()));
 runtime.SetToolProvider(tools);

这里的持有关系值得单独画一张图,因为它决定了后面 lambda 怎么捕获:

flowchart TB subgraph stack[&#34;main 栈上对象(生命周期 = main 作用域)&#34;] RT[AgentRuntime] SM[SessionMetaStore] HC[HistoryCache] AS[AgentServer] WG[WebGateway] AHS[AgentHttpServer] end subgraph heap[&#34;shared_ptr 堆对象&#34;] TS[SqliteTaskStore] MO[DeepSeekClient] TP[CompositeToolProvider] IL[ProjectInstructionsLoader] MB[MCPClientBridge] end MO -->|shared_ptr| RT TP -->|shared_ptr| RT IL -->|shared_ptr| RT TS -->|shared_ptr| AS AS --> WG AS --> AHS HC --> AS HC --> WG SM --> WG

AgentRuntime、AgentServer、WebGateway 都是栈对象,它们只活到 main() 结束 ;model、tools、task_store 用 shared_ptr 是因为它们被运行时和多个 handler 同时引用。相应地,所有回调都用 [&] 按引用捕获这些栈对象,只要不把 WebGateway 或 AgentServer 挪出 main(),生命周期就是安全的。这是一个很典型的 C++ 手写 DI 取舍:省掉了容器和所有权管理,代价是入口文件的局部变量不能随意转移。

coding_system_prompt 是硬编码在 main.cpp 里的多行字符串(src/main.cpp),内容要求模型「用户要改文件就必须在同一轮调用 write/edit,不能只给计划」。它没有走配置,属于当前的一个已知限制:想改提示词得改代码重编。演进记录里 2026-09-10 08:36 那次改动就是专门调这段文字的------早期模型经常只输出一份计划就结束,加了「不许只计划不写文件」的约束后才稳定下来。

3.4 可选的 MCP:加载失败只降级,不退出

MCP 桥是可选的,代码先看 [mcp] 段是否显式启用,没有就退回到「vendor 下有没有构建产物」的自动判断:

rust 复制代码
 if (parsed_mcp.has_value() && parsed_mcp->enabled) {
     mcp_config = *parsed_mcp;
 } else {
     mcp_config.binary = options.mcp_binary.empty()
         ? "vendor/mcp-extension-platform/build/mcp_server/mcp_server"
         : options.mcp_binary;
     // plugins_dir / python_path 同理,未指定时用 vendor 下的默认值
     mcp_config.enabled = std::filesystem::exists(mcp_config.binary);
 }
 ​
 auto mcp_bridge = std::make_shared<aiagent::mcp::MCPClientBridge>();
 if (mcp_config.enabled) {
     try {
         mcp_bridge->Start(mcp_config);
         tools->Add(mcp_bridge);            // 追加进同一个 CompositeToolProvider
         std::cout << "MCP tools loaded: " << mcp_bridge->ListTools().size() << '\n';
     } catch (const std::exception& e) {
         std::cerr << "MCP disabled: " << e.what() << '\n';
         mcp_bridge.reset();                 // 失败就丢掉,进程照常跑
     }
 } else {
     std::cout << "MCP disabled (binary not found)\n";
 }

这段的设计意图是:MCP 只是能力扩展,不是服务的前置条件 。子进程启动失败、协议握手失败都不应该让整个 Agent 起不来,丢一条 MCP disabled 日志继续跑即可。启动输出里那句 MCP disabled (binary not found) 就是这里打出来的,上一篇已经见过。要注意的是 tools->Add(mcp_bridge) 加进的是同一个 CompositeToolProvider,所以内置工具和 MCP 工具在 AgentRuntime 眼里没有区别,重名时以先加入者为准(内置工具优先)。

3.5 A2A 接线:一个 lambda 接住 SendMessage

A2A 服务端的构造只有两行:

vbscript 复制代码
 a2a::server::AgentServer agent_server(BuildAgentCard(options.port), task_store);
 agent_server.task_manager().SetMessageHandler(
     [&](const a2a::models::SendMessageRequest& request) {
         std::vector<a2a::models::Message> history =
             LoadLatestHistory(history_cache, *task_store, request.message.context_id);
         history.push_back(request.message);
         auto response = runtime.HandleMessage(request.message, history,
                                               request.message.context_id);
         if (!request.message.context_id.empty() && response.IsTask()) {
             history_cache.Put(request.message.context_id, response.GetTask().history);
         }
         return response;
     });

BuildAgentCard(options.port) 生成对外暴露的 Agent Card,其中 capabilities.streaming = false、push_notifications = false(BuildAgentCard,src/main.cpp)。也就是说,外部 A2A 客户端发现这张卡时,看到的是非流式能力;A2A 这条路径走的是同步的 HandleMessage。AgentHttpServer 本身是支持 SSE 的(A2A 库的 agent_http_server.hpp 里有说明),只是当前 card 没有声明,这里如实暴露了自己的能力边界。

回调做的事很少:读历史 → 拼上本轮用户消息 → 交给 AgentRuntime → 把返回的历史写回缓存 。真正的对话逻辑、工具循环、压缩都在 AgentRuntime 内部。这段代码还顺带说明了两个模块的边界:

  • AgentRuntime 不认识 a2a::Task,只收发 a2a::models::Message 列表;
  • TaskManager 负责补全 task id、状态流转和入库,main 只负责在成功后刷新 HistoryCache。

history_cache 是 2026-09-08 为长会话加的内存缓存,把每次请求取历史从全表扫描 ListAll()(O(n²))降成 O(1)。LoadLatestHistory 的注释里写清了一个容易踩的点:

arduino 复制代码
 // 缓存优先地取某个会话的最新历史;缓存未命中时回退存储层。
 // 选取写入时间最新的任务:手动/自动压缩会写入更短的历史,比较条数会选错。

同一个 context_id 下可能有多条 task。压缩会把更短的历史作为新 task 落库,如果按「历史条数最多」选,反而会选到压缩前的旧版本。所以这里比较的是 task.status.timestamp,取写入时间最新的那条,而不是最多的一条。这是压缩功能上线后才暴露出来的问题,属于「读代码容易忽略、但注释里已经写明」的典型细节。

3.6 对话接口接线:coding agent 走的流式路径

WebGateway 监听 18081,是后端对外的对话接口。终端 coding agent(de 拉起的 cli/cli.mjs)就是通过它工作的:cli/session.mjs 里调用 /api/v1/chat/stream 发起对话、/api/v1/chat/cancel 中断、/api/v1/sessions* 做会话管理。浏览器前端 web/node_frontend(默认监听 3000)只是把 /api/* 反向代理到 18081,再叠一个最小聊天页面,当初是为了随手验证链路,功能停在「能聊天」,不是产品重点。所以这一节的流式接线,服务的主要对象就是 coding agent。

WebGateway 的构造带两个依赖:AgentServer 和 task_store。构造之后,main 连续注册了九个 Set*Handler,按用途可以分成四组:

  • 历史与压缩 :SetHistoryCache、SetCompactHandler、SetCompactionInfoHandler;
  • 会话管理 :SetSessionMetaStore、SetSessionDetailsHandler、SetDeleteHandler、SetCloneHandler;
  • 工具清单 :SetToolsHandler;
  • 流式对话 :SetStreamEventHandler。

其中流式那条是主路径,也是整篇里最关键的接线:

rust 复制代码
 web_gateway.SetStreamEventHandler(
     [&](const std::string& session_id, const std::string& content,
         const std::string& cwd, const aiagent::agent::AgentEventCallback& on_event,
         const WebGateway::CancelCallback& should_cancel) {
         if (!cwd.empty()) {
             session_meta.SetCwd(session_id, cwd);   // 记住每个会话最近的工作目录
         }
         auto history = LoadLatestHistory(history_cache, *task_store, session_id);
         auto user = a2a::models::MakeTextMessage(a2a::models::Role::kUser,
                                                  content, "text/plain");
         history.push_back(user);
 ​
         const std::string reply = runtime.HandleMessageStreamingEvents(
                 user, history, session_id, on_event, cwd, should_cancel);
 ​
         a2a::models::Task task;
         task.id = std::string(a2a::core::NewUuid());
         task.context_id = session_id;
         task.status = a2a::models::MakeTaskStatus(a2a::models::TaskState::kCompleted);
         task.history = std::move(history);
         task.history.push_back(a2a::models::MakeTextMessage(
                 a2a::models::Role::kAgent, reply, "text/plain"));
         task_store->Put(task);
         history_cache.Put(session_id, std::move(task.history));
     });

on_event 是一个 std::function<void(const AgentEvent&)>,由 WebGateway 传进来,负责把事件转成 SSE 帧写回 HTTP 响应;should_cancel 则由客户端「取消」接口驱动(coding agent 里是流式输出期间按 Esc),让工具执行和模型轮次可以提前中断。main 只负责把这两个回调透传给 AgentRuntime,自己不关心 SSE 的格式------那是 Web 层的事。

这里有一个容易看错的地方:对话接口的同步和流式两条路,在 main 里接法并不一样 。WebGateway 的 ChatOnce(/api/v1/chat)会构造 SendMessageRequest 调 agent_server_.task_manager().SendMessage(),绕回了上面 3.5 那个 handler;而 coding agent 用的 /api/v1/chat/stream 直接调用这里注入的 SetStreamEventHandler,不经过 TaskManager。两条路最后都落到同一个 AgentRuntime 上,但前者的历史由 TaskManager 维护,后者由这段 lambda 自己 Put 落库。读 Web Gateway 那篇时,这个分叉是关键。

3.7 启动、等待信号与关闭

两个服务在主线程串行启动,Web 先、A2A 后,任一失败都直接退出并打印具体端口:

c 复制代码
 if (!web_gateway.Start("127.0.0.1", options.web_port)) {
     std::cerr << "failed to start Web Gateway on port " << options.web_port << '\n';
     return 1;
 }
 a2a::server::AgentHttpServer http_server(agent_server);
 if (!http_server.Start("127.0.0.1", options.port)) {
     std::cerr << "failed to start A2A HTTP server on port " << options.port << '\n';
     return 1;
 }
 ​
 std::signal(SIGINT, OnSignal);
 std::signal(SIGTERM, OnSignal);
 ​
 while (!g_stop_requested) {
     std::this_thread::sleep_for(std::chrono::milliseconds(100));
 }
 ​
 http_server.Stop();
 web_gateway.Stop();

g_stop_requested 是 volatile std::sig_atomic_t,信号处理函数只置位、不做其他事,主线程每 100ms 检查一次,这是信号安全的常规写法。两个服务各跑在自己的监听线程上,主线程空转等待。

关闭顺序和启动顺序相反:先停 A2A,再停 Web。WebGateway 的析构函数里也会调一次 Stop(),所以这里显式调用是为了让停止在 main 返回前完成。需要注意的是 A2A 服务端和 Web 网关的 Stop() 都只是停监听并 join 线程,并不会打断正在执行的模型请求------这一点在工具执行耗时较长时会表现为「按 Ctrl+C 后要等当前请求跑完才退出」。对本地工具可以接受,对外部署则要考虑加请求级取消。


4. 那些值得说的细节

入口文件在持续变长。 现在 main.cpp 607 行,其中一大半是 lambda:会话删除、克隆、压缩、cwd 持久化、工具清单......每加一个 REST 接口,入口就多一段。好处是「接线全在一处」,新读者从一个文件能看全整体;坏处是这个文件已经接近可维护性的边际,后续更适合把会话管理这类成组 handler 挪到独立的装配函数或一个 AppContext 里。这是现状,不是已经解决的问题。

lambda 捕获引用带来的约束。 所有 handler 都写 [&],捕获 runtime、task_store、history_cache、session_meta 等局部变量。它能省掉大量 shared_ptr,但要求这些对象活得比 handler 长。由于 handler 都注册在 main 作用域内的对象上,当前是安全的;一旦有人把 WebGateway 或 AgentServer 改成在堆上长期存在,这套捕获就会变成悬垂引用。换句话说,main 的 lambda 风格和对象的栈/堆布局是绑定的,改一处要同时改另一处。

跨线程共享状态需要自己加锁。 compaction_records 这个 std::map 被压缩回调写、被 GET /compaction 读,两者可能在不同线程上,所以 main 里配了一个 std::mutex compaction_records_mutex,两处访问都先加锁。HistoryCache 和 SqliteTaskStore 内部已各自线程安全,入口这边不需要再套锁。这也是手写 DI 的代价:哪些共享、哪些不用锁,全靠开发者自己判断,没有容器帮你标注作用域。

工具轮次的演进。 AgentRuntime 里 max_tool_rounds_ 默认是 0,表示不限制轮次,对齐主流 agent loop「只要有 tool_calls 就继续」。这个默认值是两次调整后的结果:最早版本上限 8,长任务会静默截断在半截计划上;后来改成 24 并明确区分「自然结束」和「打到上限」;再后来干脆默认不限制,只保留 SetMaxToolRounds() 给测试或显式安全限制用。入口这里没有调用它,所以线上跑的就是无上限行为。

system prompt 硬编码在入口。 coding_system_prompt 是 src/main.cpp 里的一段多行字符串,没有对应的配置项。它承载了「必须真的写文件」这种会影响成败的行为约束,却需要改代码才能调整。把提示词外置到配置文件或独立的 prompt 文件,是这个项目下一步可以改进的点之一。


5. 测试与验证

main.cpp 本身没有单元测试------它只做组装,测它意义不大。但两条链路的接线方式在测试里有最小复刻,可以用来对照:

  • tests/m0_smoke_test.cpp:AgentServer + SetMessageHandler + AgentHttpServer,再用 A2AClient 走一遍 SendMessage,验证「本地起 A2A 服务并回环」这条路径。
  • tests/web_gateway_test.cpp:AgentRuntime + SetMessageHandler + WebGateway + SetStreamHandler,验证 coding agent 依赖的 REST 与 SSE 接口。

跑测试不需要 API Key:

css 复制代码
 cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
 cmake --build build -j
 ctest --test-dir build --output-on-failure          # 期望 11/11 通过
 ctest --test-dir build -R aiagent_web_gateway -V    # 只看对话接口接线

想直接验证入口的默认值,可以 grep 出来对照启动日志:

bash 复制代码
 rg -n "18080|18081|conf/conf.ini|aiagent_tasks.db" src/main.cpp

接真实模型后,用三条 curl 做端到端自检:

bash 复制代码
 ./build/aiagent_app --config conf/conf.ini --db build/db/aiagent_tasks.db &
 curl -s http://127.0.0.1:18081/health
 curl -s http://127.0.0.1:18081/api/v1/tools
 curl -s http://127.0.0.1:18080/.well-known/agent-card.json

Agent Card 返回的 JSON 里 capabilities.streaming 是 false,和 3.5 节讲的 BuildAgentCard 一致;如果哪天改成 true,说明入口的能力声明变了,值得回头核对实现。


6. 小结

  1. main.cpp 是手写的依赖装配:先解析选项、装载配置、启动日志,再创建持久化与模型,然后构造 AgentRuntime 并注入工具/指令/压缩,最后注册回调、启动两个服务。
  2. 依赖方向很清晰:AgentRuntime 只认 Message 列表和 IModelClient / IToolProvider 抽象,A2A 与对话接口都是它的上层入口;外部 Agent 走 A2A 的同步 SendMessage,coding agent 走 WebGateway 的流式 SetStreamEventHandler。
  3. 失败策略分两级:DeepSeek 配置缺失直接退出,MCP 加载失败只降级;跨线程共享的 compaction_records 显式加锁,历史读取按写入时间而非条数取最新。
  4. 入口的代价是它越来越长、system prompt 硬编码、lambda 捕获引用与对象生命周期绑定,这几处是后续可以改进的地方。

下一篇进入 DeepSeekClient,看 main.cpp 交给 AgentRuntime 的那个 IModelClient 背后,同步、SSE 流式和 tool_calls 分别是怎么实现的。

相关推荐
zed_231 小时前
省钱之道:token 成本日志 + FAQ 缓存
后端
量化分析码农2 小时前
【Python量化因子实战 #07】因子加权怎么选?等权 / IC 加权 / 最大化 IR 三种策略 PK
后端
Wx-bishekaifayuan2 小时前
springboot甘肃特产服务平台50301-计算机课程设计、毕业设计
spring boot·后端·python·django·课程设计·express·旅游
Thneonl2 小时前
服务不通先别抓包:DNS 解析器的坑比内核网络栈多
后端·程序员
Thneonl2 小时前
一启动就 Exited(137):内存限制背后藏了四个参数
后端·架构
斯维赤2 小时前
从 @Tool 到 Agent 流水线:LangChain4j 进阶教学,一个库打全套
java·后端
对象存储与RustFS2 小时前
RustFS 生产部署实战:Docker / 二进制 / K8s Operator 三条路径怎么选
后端
知守观2 小时前
Spring Boot 2.1.0 停维护5年,10个依赖4个有CVE——2022年老项目的安全体检报告
spring boot·后端
SimonKing2 小时前
SSE项目`nexus-sse`持续优化,不一样的视觉效果
java·后端·程序员