一、背景与问题
在闪购搜索团队的日常工作中,我们需要频繁地进行搜索问题排查、性能分析、实验管理等操作。这些操作分散在多个平台(SLS日志、TPP实验平台、代码仓库等),效率低下。
我们的目标是:在钉钉群里直接对话一个AI助手,它能代替人去查日志、看实验、分析性能、甚至部署代码。
然而面临几个核心挑战:
| 挑战 | 具体描述 |
| 内网部署限制 | 服务部署在内网,无法暴露公网回调地址,传统Webhook方案不可行 |
| 实时性要求 | AI推理耗时较长(30s-120s),用户无法接受"发消息→等2分钟→一次性返回" |
| 安全性要求 | 需要权限隔离,非管理员不能执行写操作(部署代码、修改配置等) |
| 工具集成需求 | 需要访问代码仓库、日志系统、实验平台等多种外部工具 |
二、方案概览
2.1 整体架构
我们最终采用 "钉钉 Stream + CLI 代理" 的方案:
┌────────────────────────────────────────────────────────────────┐ │ 钉钉群 / 单聊 │ ├────────────────────────────────────────────────────────────────┤ │ 钉钉 Stream WebSocket 长连接(内网直通,无需公网回调) │ ├────────────────────────────────────────────────────────────────┤ │ Java 服务(alsc-intervene) │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ DingTalkStreamService │ │ │ │ • 权限校验(管理员 vs 只读用户) │ │ │ │ • 上下文管理(LRU + TTL + 滑动窗口) │ │ │ │ • 并发控制(线程池 10-15) │ │ │ │ • AI卡片投放与流式更新 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ CLI 代理层(ProcessBuilder) │ │ │ │ • Qoder CLI / Claude Code(可切换) │ │ │ │ • stdbuf -oL 行缓冲优化 │ │ │ │ • 120s 超时保护 + 异常杀进程 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ MCP Server(工具调用层) │ └────────────────────────────────────────────────────────────────┘
2.2 关键技术选型
| 组件 | 方案 | 选择理由 |
| 消息通道 | 钉钉 Stream(WebSocket) | 内网无需公网回调地址,完全规避DNS/防火墙限制 |
| AI引擎 | CLI 代理模式 | 轻量无框架依赖,复用成熟 CLI 生态 |
| 流式展示 | 钉钉 AI 卡片 | 原生支持打字机效果,用户体验好 |
| 进程管理 | Java ProcessBuilder | 进程级隔离,一个请求一个进程,互不影响 |
| 工具扩展 | MCP(Model Context Protocol) | 标准化协议,可插拔配置驱动 |
| 上下文存储 | 内存 LinkedHashMap | 自动 LRU 淘汰,无需外部存储 |
三、引擎选型:从 Qoder CLI 到 Claude Code
3.1 最初选择 Qoder CLI
项目最初选择 Qoder CLI 作为 AI 引擎,主要考虑:
- 内部产品,接入方便,有现成的 Skills 和 MCP 生态
- CLI 模式天然适合服务端 spawn 调用
- 支持
stream-json流式输出
3.2 遇到的问题
实际使用中发现,Qoder CLI 在复杂问题排查场景下的表现不如预期:
- 对于需要多步推理的复杂排查(如跨系统问题定位),回答质量有时不够准确
- 与 Qoder IDE 桌面端相比,CLI 模式的能力有一定差距
3.3 引入 Claude Code
为了提升复杂场景下的回答质量,我们后续引入了 Claude Code 作为替代引擎。实际效果验证:
- 复杂问题排查能力显著更强:对于涉及多系统、多步骤的排查场景,Claude Code 的推理深度和准确性明显优于之前的方案
- MCP 工具调用更稳定:工具调用的成功率和参数构造准确度更高
目前两个引擎并行部署,通过不同的入口调用:
- Qoder CLI:钉钉 Stream 入口(群聊场景)
- Claude Code:HTTP SSE 入口 + 独立钉钉机器人
四、Docker 部署方案
4.1 部署架构
两个引擎部署在同一个 Docker 容器内,共享工作目录和 MCP 配置:
Docker 容器 ├── /home/admin/qoder-workspace/ # 共享工作目录 │ ├── .mcp.json # MCP服务配置 │ ├── AGENTS.md # Qoder CLI 知识入口 │ └── CLAUDE.md # Claude Code 指令文件 ├── /home/admin/.qoder/ # Qoder CLI 专属配置 │ ├── settings.json # MCP server列表 │ ├── mcp-oauth-tokens.json # OAuth认证token │ └── skills/ # 安装的Skills ├── /home/admin/.claude/ # Claude Code 专属配置 │ └── settings.json # 环境变量 + 权限 └── /opt/mcp-auth/ # Claude MCP认证文件
4.2 Dockerfile 核心设计
FROM reg.docker.alibaba-inc.com/alibase/alios7u2-min # 基础运行时 RUN yum install -y ajdk11-11.0.27.26 nodejs git # 安装双引擎 RUN npm install -g @qoder-ai/qodercli \ --registry=https://registry.npm.alibaba-inc.com && \ npm install -g @ali/claude-code@2.1.144 \ --registry=https://registry.npm.alibaba-inc.com # 部署工作区 COPY qoder-workspace/ /home/admin/qoder-workspace/ # Qoder CLI 认证 COPY qoder-home/.qoder/ /home/admin/.qoder/ RUN chmod 600 /home/admin/.qoder/mcp-oauth-tokens.json && \ chmod 444 /home/admin/.qoder/mcp-oauth-clients.json # Claude Code 认证 COPY claude-home/.claude/ /home/admin/.claude/ COPY claude-mcp-auth/ /opt/mcp-auth/ RUN ln -sf /opt/mcp-auth /home/admin/.mcp-auth
4.3 Java 层调用方式
两个引擎的调用方式几乎相同,都是通过 ProcessBuilder spawn 子进程:
// Qoder CLI ProcessBuilder pb = new ProcessBuilder( "stdbuf", "-oL", "qodercli", "-p", prompt, "--output-format", "stream-json", "--yolo", "--max-turns", "10", "-w", "/home/admin/qoder-workspace" ); pb.environment().put("QODER_PERSONAL_ACCESS_TOKEN", qoderToken); // Claude Code ProcessBuilder pb = new ProcessBuilder( claudeCliPath, "-p", prompt, "--output-format", "stream-json", "--verbose", "--max-turns", "5" ); pb.environment().put("ANTHROPIC_AUTH_TOKEN", anthropicToken);
两者输出格式统一为 stream-json,Java 层的流式解析、AI卡片更新逻辑完全复用。
五、MCP 工具集成与 OAuth 认证跳过方案
5.1 MCP 是什么
MCP(Model Context Protocol)是 AI 调用外部工具的标准化协议。通过 MCP,AI 可以像人一样去调用各种平台的 API——查代码、看日志、管实验,实现"问一句话,帮你查完所有系统"的效果。
我们从本地常用的 MCP 服务中选择了一部分部署到远端,覆盖代码仓库、日志查询、实验管理等核心场景。
5.2 MCP 认证问题
MCP 服务默认采用 OAuth2 认证流程:
- 客户端向 MCP 网关发起请求
- 网关返回 401 + OAuth discovery URL
- 客户端走 OAuth 授权码流程获取 access_token
- 后续请求携带 Bearer token
这个流程在本地开发时没问题(浏览器打开授权页面点击授权),但在无头服务器(Docker容器)上完全行不通——没有浏览器,无法完成交互式授权。
5.3 跳过 OAuth 的方案:静态 Bearer Token
我们的解决方案是预先获取 token,以静态方式注入配置文件,跳过运行时的 OAuth 流程:
// .mcp.json - 直接在 headers 中携带 Bearer token { "mcpServers": { "code": { "type": "streamable-http", "url": "https://mcp.alibaba-inc.com/code/mcp", "headers": { "Authorization": "Bearer mcpa_xxxxxxxxxxxxxxxx" } }, "sls-mcp": { "type": "streamable-http", "url": "https://mcp.alibaba-inc.com/sls-mcp/mcp", "headers": { "Authorization": "Bearer mcpa_yyyyyyyyyyyyyyyy" } } } }
工作原理:
- 本地预获取:在本地开发环境通过正常 OAuth 流程获取
access_token(mcpa_前缀的长期token) - 静态注入:将 token 写入
.mcp.json的headers.Authorization字段 - 直接认证:CLI 发起 MCP 请求时直接携带此 header,MCP 网关验证 token 有效即放行,完全跳过 OAuth 握手
- Docker 构建时打包:
.mcp.json在 Dockerfile 中 COPY 进容器,启动即可用
Token 管理要点:
- Token 有有效期,过期后需要重新获取并更新配置
.mcp.json文件权限设为 600,防止泄露mcp-oauth-clients.json设为 444 只读,防止运行时被 DCR(Dynamic Client Registration)覆盖
5.4 Qoder CLI 的 OAuth Token 方式
Qoder CLI 还支持另一种认证方式——通过 mcp-oauth-tokens.json 文件存储 OAuth token:
[ { "serverName": "code", "token": { "accessToken": "mcpa_xxxxx", "refreshToken": "mcpr_xxxxx", "expiresAt": 1782186490063, "tokenType": "Bearer" }, "clientId": "mcp-client-xxxx", "tokenUrl": "https://mcp.alibaba-inc.com/oauth/token", "mcpServerUrl": "https://mcp.alibaba-inc.com/code/mcp" } ]
理论上 refreshToken 可以自动续期,但实测发现远端环境中 token 刷新不够可靠,因此 Claude Code 端我们统一使用 .mcp.json 静态 headers 方案,更加稳定。
六、钉钉 Stream 集成核心实现
6.1 为什么选择 Stream 模式
传统钉钉机器人采用 HTTP 回调,要求服务有公网可达地址。Stream 模式通过 WebSocket 长连接解决了这个问题——服务主动连接钉钉服务器,无需暴露任何端口。
@PostConstruct public void init() { if (!streamEnabled) return; OpenDingTalkClient client = OpenDingTalkStreamClientBuilder.custom() .credential(new AuthCredential() {{ setClientId(appKey); setClientSecret(appSecret); }}) .registerCallbackListener("/v1.0/im/bot/messages/get", this) .build(); client.start(); }
6.2 进程管理与流式输出
public void chatStream(String prompt, String workspace, Consumer<String> lineConsumer, Consumer<Process> processConsumer) { List<String> cmd = Arrays.asList( "stdbuf", "-oL", // 强制行缓冲 cliPath, "-p", prompt, "--output-format", "stream-json", "--yolo", "--max-turns", "10", "-w", workspace ); ProcessBuilder pb = new ProcessBuilder(cmd); pb.environment().put("QODER_PERSONAL_ACCESS_TOKEN", token); pb.redirectErrorStream(true); Process process = pb.start(); processConsumer.accept(process); try (BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream()), 256)) { String line; while ((line = reader.readLine()) != null) { lineConsumer.accept(line); } } if (!process.waitFor(120, TimeUnit.SECONDS)) { process.destroyForcibly(); } }
关键设计点:
- **
stdbuf -oL**:强制行缓冲,避免 Node.js 4KB 全缓冲导致的延迟 - 256字节 BufferedReader:Java 侧小缓冲确保及时输出
- 异常即杀进程:consumer 异常立即 destroyForcibly,停止消耗推理额度
- 进程引用暴露:支持用户发"停止"命令时主动中断
6.3 AI卡片流式更新
// 频率控制:累计超50字符才更新一次 if (fullContent.length() - lastUpdateLen > 50) { String content = fullContent.length() > 3000 ? fullContent.substring(0, 3000) : fullContent.toString(); cardService.streamUpdate(trackId, content, false, false); lastUpdateLen = fullContent.length(); }
6.4 用户上下文管理(三重防护)
private final LinkedHashMap<String, UserContext> contextMap = new LinkedHashMap<>(16, 0.75f, true) { @Override protected boolean removeEldestEntry(Entry<String, UserContext> eldest) { return size() > 500; } };
| 保护层 | 机制 | 触发条件 | 效果 |
| 第一层 | TTL过期 | 48小时无活动 | 清空上下文 |
| 第二层 | 滑动窗口 | 单用户超200KB | FIFO删除最早对话 |
| 第三层 | LRU淘汰 | 全局超500用户 | 淘汰最久未使用 |
6.5 权限隔离
- 管理员:完全权限(读写+命令执行+部署)
- 普通用户:只读模式(系统指令最高优先级强制约束)
6.6 并发控制
private final ExecutorService qoderExecutor = new ThreadPoolExecutor( 10, 15, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(30), new ThreadPoolExecutor.AbortPolicy() );
七、知识自进化机制
五级知识沉淀模型,让 AI 越用越聪明:
L0 git history(自动追踪代码变更) ↓ L1 .qoder/context/(每次任务的过程报告) ↓ L2 .qoder/memory_recent.md(最近5次会话摘要) ↓ L3 .qoder/rules/candidates/(候选规则,经验沉淀) ↓ L4 AGENTS.md + P0-constraints.md(正式规则)
候选规则触发 ≥3 次且成功率 ≥80%,自动提议晋升为正式规则。
八、钉钉机器人配置与申请指南
8.1 创建机器人应用
- 登录 钉钉开放平台
- 创建企业内部应用 → 选择"机器人"类型,选群聊机器人即可(只需要主管审批)
- 获取
appKey和appSecret
8.2 开通必要权限
目前看下来内部申请的机器人默认都开通了下面三项重要的权限,如果有遗漏,可能得单独申请一下
| 权限名称 | 用途 |
|----------|------|
| 企业内机器人发送消息 | 主动回复用户消息 |
| 互动卡片实例写权限 | 创建和投放AI卡片 |
| AI卡片流式更新权限 | 实现打字机效果 |
8.3 启用 Stream 模式
- 机器人配置页 → "消息接收模式" → Stream 模式
- 无需填写回调URL
- 发布到组织内
8.4 AI卡片模板配置
- 进入 卡片平台
- 新建卡片模板 → 场景选"消息卡片 + AI卡片"
- 开启"流式组件"开关
- 记录模板ID配置到
application.properties
8.5 关键配置项
dingtalk.stream.enabled=true # Stream开关(预发/线上设false) dingtalk.app.key=${DINGTALK_APP_KEY} # antx注入 dingtalk.app.secret=${DINGTALK_APP_SECRET} dingtalk.robot.code=${DINGTALK_ROBOT_CODE}
8.6 注意事项
- 多实例部署需通过环境开关控制,只在日常环境开启 Stream,否则消息会重复处理
- 凭证必须通过 antx/diamond 注入,禁止硬编码
- AI卡片需选择"AI卡片"场景并开启流式组件
九、运行效果展示
9.1 性能追踪
9.2 问题场景
9.3 需求跟进
(还需要继续验证下模型能力的上限,目前用qoder cli试过不太稳,但也能处理一些基本问题,复杂问题回答可能会跟本地的qoder有偏差,claude试下来效果更好~)
十、踩坑经验
10.1 stdbuf 行缓冲是必须的
Node.js 进程在非 TTY 环境下默认全缓冲(4KB),不加 stdbuf -oL 用户会看到"卡住→突然一大段"的糟糕体验。
10.2 MCP OAuth Token 格式要求
mcp-oauth-tokens.json 必须是 JSON 数组格式 [{...}],对象格式会导致 crash。
10.3 Stream 模式多实例冲突
多容器实例同时开启 Stream 监听会导致消息重复处理,需用环境开关精确控制。
10.4 AI 卡片权限
流式更新需单独申请"AI卡片流式更新权限",否则返回 403。
10.5 Claude Code 权限透传
Claude Code 的 -p 模式不会自动应用 settings.json 中的 permissions,需通过 --allowedTools 参数显式传递工具权限。
十一、总结
本方案通过 钉钉 Stream + CLI 代理 的架构,实现了:
- 完全内网部署:WebSocket 长连接规避公网回调
- 实时流式回复:stdbuf + AI卡片打字机效果
- 安全权限隔离:管理员/只读双模式
- MCP 工具开放:静态 Bearer token 跳过 OAuth,实现无头环境下的工具调用
- 引擎可切换:从 Qoder CLI 到 Claude Code,复杂排查能力显著提升
- 生产级稳定:线程池+超时+LRU 三层保护
以最低的侵入度、最轻的工程成本,实现了企业级 AI 助手从零到一的落地。由于代码中涉及了很多私人的配置项,不直接公开了,感兴趣的同学可以私我拿代码。
来源 | 阿里云开发者公众号
作者 | 董元浩(久梦)