企业级Agent Skills落地实战:混合架构保障LLM灵活性与系统稳定性

避免Agent Skills“裸奔”,构建确定性与灵活性共存的企业级混合架构,保障系统稳定与安全。

原文标题:Agent Skills 落地实战:拒绝“裸奔”,构建确定性与灵活性共存的混合架构

原文作者:AI前线

冷月清谈:

本文介绍了在企业级“智能文档分析 Agent”项目中,如何避免“纯 Skills”路线的陷阱,构建一个既能发挥 LLM 的灵活性,又能保证系统稳定性和安全性的混合架构。文章详细阐述了采用 Java (确定性 ETL) + DSL 封装式 Skills + 实时渲染的混合架构,通过 Java 主控数据流转与安全检查,LLM 负责意图理解与代码组装,Python 沙箱负责在受控环境下执行具体计算,实现了架构分层。文章还分享了在 Spring AI 体系下实现这套混合架构的关键逻辑,包括动态技能注入和业务调度与意图分流。核心在于通过 DSL 封装,收回 LLM 的“底层操作权”,只保留其“逻辑调度权”,并利用Java的确定性来弥补LLM在工程细节上的不足。总结强调了不要高估 LLM 的 Coding 能力,以及架构分层的重要性。

怜星夜思:

1、文章中提到的“DSL 封装式 Skills” 方案,在实际应用中,如何平衡 DSL 的通用性和特定业务场景的需求?
2、文章强调 Java 在数据流转和安全检查中的作用,那么在 Serverless 架构下,如何发挥 Java 的这些优势?
3、文章中提到对LLM输出做二次安检,感觉很有必要,但是这会不会影响Agent的效率和灵活性?有什么好的优化方案吗?

原文内容

作者 | 仇智慧
策划 | 李冬梅
随着 Anthropic 开源 skills 仓库,"Code Interpreter"(代码解释器)模式成为 Agent 开发的热门方向。许多开发者试图采取激进路线:赋予 LLM 联网和 Python 执行权限,让其现场编写代码来解决一切问题。但在构建企业级“智能文档分析 Agent”的实践中,我们发现这种“全托管”模式在稳定性、安全性和可控性上存在巨大隐患。本文将分享我们如何摒弃激进路线,采用 Java (确定性 ETL) + DSL 封装式 Skills + 实时渲染 的混合架构,在保留 LLM 灵活性的同时,确保系统的工业级稳定性。
背景:当文档分析遇到“复杂生成”

在我们的“文档处理 Agent”项目中,基础的问答功能(RAG)已经解决得很好。但随着用户需求升级,我们面临了新的挑战:

用户场景

“这是 2024 和 2025 年的两份经营数据报表,请对比 DAU 和营收的同比增长率,并生成一个 Excel 表格给我。另外,把总结报告导出为 PDF。”

这类需求包含两个特征:

  • 逻辑计算:需要精确算术(LLM 弱项)。

  • 文件 IO:需要生成物理文件(LLM 无法直接做到)。

引入 Skills(让 LLM 调用 Python 代码)似乎是唯一解。但在具体落地时,我们走了一段弯路。

弯路:激进的“纯 Skills”路线

起初,我们参考了开源社区做法,采用了 完全的 Code Interpreter 模式。我们将 requestspandasreportlab 等库的权限全部开放给 LLM,并在 Prompt 中告诉它:“你是一个 Python 专家,请自己写代码解决所有问题。”

这种“裸奔”模式在生产环境中遭遇了三次暴击:

  • 输入端不可控:LLM 对非结构化数据(如无后缀 URL、加密 PDF)的处理极其脆弱,经常陷入报错死循环。

  • 输出端崩坏:让 LLM 从零绘制 PDF/Word 是灾难。经常出现中文乱码、表格对不齐、使用了过期的库 API 等问题。

  • 安全黑洞:数据流完全在沙箱内闭环,Java 主程序失去了对内容的控制权,无法拦截敏感词或违规数据。

 变革:Java 主控 + 

DSL Skills 的混合架构

为了解决上述问题,我们重构了架构。核心思想是:收回 LLM 的“底层操作权”,只保留其“逻辑调度权”。

我们制定了新的架构分工:Java 负责确定性的数据流转与安检,LLM 负责意图理解与代码组装,Python 沙箱 负责在受控环境下执行具体计算。

架构设计概览

我们将系统重新划分为四个逻辑层级:

  • ETL 层 (Java):负责下载、MIME 识别、OCR、敏感词检测。这是“确定性管道”。

  • Brain 层 (LLM):负责阅读纯文本,进行逻辑推理,并生成调用代码。

  • Skills 层 (Python Sandbox):提供高度封装的 SDK(DSL),而非裸库。

  • Delivery 层 (Java):负责将 Markdown/HTML 实时渲染为 PDF/Word。

输入侧:回归 Java 流水线 (ETL)

我们不再让 LLM 去下载和解析文件。所有输入文件,先经过 Java 的 DocPipeline。利用 Apache Tika 进行精准解析,并立即进行 敏感词检测 和 文本截断。这一步保证了 喂给 LLM 的数据是干净、安全、标准化的纯文本

3.3 中间层:DSL 封装模式 (The Wrapper Pattern)

这是我们对 Skills 实践最大的改进。我们 禁止 LLM 直接写 import pandas 进行底层操作,而是预置了一套高度封装的 DSL。

Python 端封装 (excel_tool.py):

import pandas as pd
import os
def create_excel(data_list, filename="report.xlsx", output_dir="/workspace"):
    try:
        df = pd.DataFrame(data_list)
        save_path = os.path.join(output_dir, filename)
        # <span class="specialTitle">【封装价值体现】</span>自动处理格式、列宽、引擎兼容性,屏蔽 LLM 的幻觉风险
        with pd.ExcelWriter(save_path, engine='openpyxl') as writer:
            df.to_excel(writer, index=False, sheet_name='Sheet1')
            # 自动调整列宽 (LLM 很难写对的工程细节)
            worksheet = writer.sheets['Sheet1']
            for idx, col in enumerate(df.columns):
                max_len = max(df[col].astype(str).map(len).max(), len(str(col))) + 2
                worksheet.column_dimensions[chr(65 + idx)].width = min(max_len, 50)
        return save_path
    except Exception as e:
        return f"Error: {str(e)}"

Skill 说明书 (SKILL.md):

我们在 Prompt 中通过“接口契约”强行约束 LLM 的行为,明确了何时该写代码,何时该纯输出文本。

# File Generation Skill (Standardized)
你拥有生成专业格式文件(Excel, Word, PDF)的能力。
沙箱中已预装了封装好的 `excel_tool` 库。
**核心决策树**:
1. 如果是 **统计数据/表格** -> 必须生成 **Excel** -> **写 Python 代码**。
2. 如果是 **分析报告/文档** -> 必须生成 **Word/PDF** -> **禁止写代码**,走渲染路径。
---
### 场景 1:生成 Excel (.xlsx)
**规则**:禁止使用 `pandas` 底层 API,必须调用封装函数。
**数据结构**:必须是【字典列表】,每个字典代表一行。
**Python 调用示例**:
```python
import excel_tool
# 1. 准备数据 (从文档中提取)
data = [
    {'年份': '2024', 'DAU': 1000, '营收': '500万'},
    {'年份': '2025', 'DAU': 1500, '营收': '800万'}
]
# 2. 调用封装函数 (自动处理样式、列宽)
excel_tool.create_excel(data, filename='analysis.xlsx')
```
---
### 场景 2:生成 Word / PDF (.docx / .pdf)
**规则**:**严禁编写 Python 代码**(如 `reportlab` 或 `python-docx`)。
**执行动作**:
1. 请直接输出内容丰富、排版精美的 **Markdown** 文本。
2. 在 Markdown 的**最后一行**,务必添加对应的动作标签,系统会自动将其渲染为文件。
**输出示例**:
# 2024 年度经营分析报告
## 一、 数据概览
本季度营收同比增长 20%...
| 指标 | Q1 | Q2 |
| :--- | :--- | :--- |
| DAU | 100w | 120w |
... (此处省略 2000 字内容) ...
<<<ACTION:CONVERT|pdf>>>
# File Generation Skill (Standardized)

你拥有生成专业格式文件(Excel, Word, PDF)的能力。
沙箱中已预装了封装好的 `excel_tool` 库。

** 核心决策树 **:
1. 如果是 ** 统计数据 / 表格 ** -> 必须生成 **Excel** -> ** 写 Python 代码 **。
2. 如果是 ** 分析报告 / 文档 ** -> 必须生成 **Word/PDF** -> ** 禁止写代码 **,走渲染路径。



#### 场景 1:生成 Excel (.xlsx)
** 规则 **:禁止使用 `pandas` 底层 API,必须调用封装函数。
** 数据结构 **:必须是<span class="specialTitle">【字典列表】</span>,每个字典代表一行。

**Python 调用示例 **:
```python
import excel_tool

# 1. 准备数据 (从文档中提取)
data = [
    {'年份': '2024', 'DAU': 1000, '营收': '500 万'},
    {'年份': '2025', 'DAU': 1500, '营收': '800 万'}
]

# 2. 调用封装函数 (自动处理样式、列宽)
excel_tool.create_excel(data, filename='analysis.xlsx')

输出侧:渲染与交付的分离

对于不同类型的文件,我们采取了截然不同的交付策略:

  1. Excel(强结构化):走 Skills 路线。LLM 组装数据 -> 调用 excel_tool -> 沙箱生成物理文件。

  2. Word/PDF(富文本):走 渲染路线严禁 LLM 写代码生成。

    1. LLM 只输出高质量的 Markdown 并在末尾打上 <<<ACTION:CONVERT|pdf>>> 标签。

    2. Java 后端拦截该标签,利用 OpenHTMLtoPDF 或 Pandoc 将 Markdown 实时转换 为精美的 PDF/Word。

硬核代码实现 (Spring AI)

以下是我们在 Spring AI 体系下实现这套混合架构的关键逻辑。

动态技能注入 (SkillManager)

我们实现了一个 SkillManager,支持按需加载技能。为了提升性能,我们设计了 Session 级的“防抖机制”,确保同一个会话中只需上传一次 Python 脚本,避免重复 IO。

@Service
public class SkillManager {
    // 缓存技能脚本: 技能名 -> { 文件路径 -> 内容 }
    private final Map<String, Map<String, String>> skillScripts = new ConcurrentHashMap<>();
    // 防止重复注入的防抖 Set
    private final Set<String> injectedSessions = ConcurrentHashMap.newKeySet();
    /**
     * 核心逻辑:根据需要的技能列表,动态注入脚本到沙箱
     */
    public void injectToSandbox(String sessionId, List<String> neededSkills) {
        // 1. 防抖检查:如果该 Session 已注入,直接跳过,避免重复 IO
        if (injectedSessions.contains(sessionId)) return;
        // 2. 注入 Python 包结构 (__init__.py)
        sandboxService.uploadFile(sessionId, "/workspace/skills/__init__.py", "");
        // 3. 批量上传该技能所需的 DSL 脚本
        for (String skillName : neededSkills) {
            Map<String, String> scripts = skillScripts.get(skillName);
            if (scripts != null) {
                scripts.forEach((path, content) -> 
                    sandboxService.uploadFile(sessionId, path, content)
                );
            }
        }
        injectedSessions.add(sessionId);
    }

    // … 省略加载 Resource 的代码 …
}

业务调度与意图分流 (Handler)

串联 Java ETL、LLM 推理和最终的交付分流。

@Service
public class DocumentAnalysisRequestHandler {
    public Flowable<Response> processStreamingRequest(Request req) {
        // 1. 【Java ETL】确定性解析与安检
        // 无论 URL 还是文件,先转为纯文本,并做敏感词过滤
        List<ParseResult> parsedDocs = etlPipeline.process(req.getUrls());

        // 2. 【技能注入】
        List<String> neededSkills = List.of(“file_generation”);
        skillManager.injectToSandbox(req.getSessionId(), neededSkills);
        // 3. 【LLM 执行】Context Stuffing
        String prompt = buildPrompt(parsedDocs, skillManager.getPrompts(neededSkills));

        // 调用 LLM,挂载 ToolContext 以实现多租户隔离
        Flowable<AgentOutput> agentFlow = chatClient.prompt()
                .system(prompt)
                .user(req.getUserInstruction())
                .toolContext(Map.of(“projectId”, req.getSessionId())) 
                .stream()
                .content();
        // 4. 【结果分流】
        return agentFlow
                .toList() // 收集完整回复
                .flatMap(this::handlePostGenerationAction);
    }
    /**
     * 核心分流逻辑:决定是返回沙箱文件(Excel) 还是 调用Java渲染(PDF)
     */
    private Single<AgentOutput> handlePostGenerationAction(List<String> rawChunks) {
        String text = String.join(“”, rawChunks);
        // 分支 A:检测到 Python 生成了 Excel (Skills 产物)
        // 格式:[FILE_GENERATED: /workspace/report.xlsx]
        if (FILE_GENERATED_PATTERN.matcher(text).find()) {
            String path = extractPath(text);
            return Single.just(new AgentOutput(path, OutputType.FILE));
        }
        // 分支 B:检测到转换指令 (渲染产物)
        // 格式:<<<ACTION:CONVERT|pdf>>>
        if (text.contains(“<<<ACTION:CONVERT|pdf>>>”)) {
            // Java 侧实时渲染:Markdown -> PDF
            // 优势:完美控制字体和样式,避免 Python 生成乱码
            String pdfPath = docConverterService.convertAndSave(text, “pdf”);
            return Single.just(new AgentOutput(pdfPath, OutputType.FILE));
        }
        // 分支 C:普通文本
        return Single.just(new AgentOutput(text, OutputType.TEXT));
    }
}

拦截与交付 (SandboxTools)

在 Tool 执行层做最后一道防线:输出内容的二次安检

@Component
public class SandboxTools {
    @Tool(name = "execute_command", description = "在沙箱中执行 Shell 命令")
    public String executeCommand(ExecuteCommandRequest req, ToolContext context) {
        String projectId = (String) context.getContext().get("projectId");
        try {
            // 1. 执行 Python 脚本
            Map<String, Object> result = sandboxMcpService.executeCommand(projectId, req.command());
            String stdout = (String) result.get("stdout");
            // 2. <span class="specialTitle">【关键】</span>输出侧安检
            // 防止 LLM 通过代码计算出违规内容,绕过输入侧检查
            if (banwordService.hasBanWords(stdout)) {
                log.warn("Banword detected in sandbox output!");
                throw new BanwordException("敏感内容阻断");
            }
            // 3. 超长截断 (防止 LLM 上下文爆炸)
            if (stdout.length() > MAX_TEXT_LENGTH) {
                return stdout.substring(0, MAX_TEXT_LENGTH) + "\n[SYSTEM: TRUNCATED]";
            }
            return stdout;
        } catch (Exception e) {
            return "Execution Error: " + e.getMessage();
        }
    }
}
总    结

Skills 技术让 LLM 拥有了“手”,但这双手必须戴上“手套”。

通过这次架构演进,我们得出的核心经验是:

  • 不要高估 LLM 的 Coding 能力:它是一个优秀的逻辑推理引擎,但在工程细节(排版、库依赖、环境配置)上非常糟糕。DSL 封装是必须的。

  • 不要丢掉 Java 的确定性:解析、下载、格式转换、安全检查,这些传统代码擅长的领域,不要交给概率性的 LLM 去做。

  • 架构分层

    • Input: Java (Standardization & Security)

    • Thinking: LLM (Reasoning)

    • Action: Python (Calculation via DSL)

    • Output: Java (Rendering & Delivery)

这种混合架构,既保留了 Agent 处理复杂动态需求的能力(如自定义计算涨跌幅),又守住了企业级应用对稳定性与合规性的底线。

点击底部阅读原文访问 InfoQ 官网,获取更多精彩内容!

会议推荐

InfoQ 2026 全年会议规划已上线!从 AI Infra 到 Agentic AI,从 AI 工程化到产业落地,从技术前沿到行业应用,全面覆盖 AI 与软件开发核心赛道!集结全球技术先锋,拆解真实生产案例、深挖技术与产业落地痛点,探索前沿领域、聚焦产业赋能,获取实战落地方案与前瞻产业洞察,高效实现技术价值转化。把握行业变革关键节点,抢占 2026 智能升级发展先机!

今日荐文

图片

你也「在看」吗?👇

我倾向于使用“规则引擎”来实现二次安检。将安全规则配置化,可以灵活地调整安检策略,而无需修改代码。同时,可以对规则引擎进行优化,比如使用缓存、索引等技术,来提升规则匹配的效率。另外,还可以引入机器学习模型,自动学习和识别违规内容,从而进一步提升安检的准确性和效率。

我觉得可以考虑使用 GraalVM Native Image 技术,将 Java 应用编译成本地镜像,从而大幅提升 Serverless 函数的启动速度和资源利用率。这可以有效解决 Java 在 Serverless 场景下启动慢的问题。另外,还可以利用 Serverless 平台的安全机制,比如 IAM 角色、VPC 等,来进一步加强数据安全。

这个问题确实很关键。二次安检肯定会带来性能损耗,但安全是底线。我的想法是,可以采用“分层安检”的策略。首先,在 Agent 的核心流程中,只进行最基本的安检,比如关键词过滤、长度限制等。然后,将更复杂的安检逻辑放到异步任务中执行,比如调用外部的审核服务、进行深度内容分析等。这样可以在不影响 Agent 核心功能的情况下,最大程度地保障安全。

这个问题问到了点子上。Serverless 的核心在于将计算能力函数化,因此,我们可以将 Java 擅长的确定性 ETL 和安全检查能力封装成一个个独立的 Serverless 函数。比如,文件解析函数、敏感词过滤函数等。然后,通过编排这些函数,构建一个 Serverless 化的数据处理管道。这种方式既能发挥 Java 的优势,又能享受 Serverless 带来的弹性伸缩和成本优化。

别忘了容器技术!可以将 Java 应用容器化,然后部署到 Serverless 容器平台上,比如 AWS Fargate、Google Cloud Run 等。这种方式可以让我们平滑地将现有的 Java 应用迁移到 Serverless 架构上,同时也保留了容器带来的灵活性和可移植性。

与其纠结于DSL的通用性,不如拥抱变化。我觉得可以考虑使用“DSL生成器”这样的工具,根据不同的业务场景,动态生成不同的DSL。这样可以最大程度地满足特定需求,同时也避免了DSL过于臃肿。当然,这种方案的复杂度也会相应提高。

这个问题很有意思!我觉得DSL的通用性和场景需求本身就是一对矛盾。我的理解是,要做好平衡,首先需要充分调研业务场景,识别出高频共性的操作,将这些操作封装成DSL的基础能力。其次,要允许DSL有一定的扩展性,比如预留一些“自定义接口”,让开发者可以针对特定场景编写少量代码来补充DSL的功能。另外我觉得可以考虑DSL的版本管理,不同版本对应不同的业务场景,这样可能更灵活。

从我的角度来看,DSL的设计应该由浅入深,一开始先提供最核心、最常用的功能,满足80%的需求。剩下的20%可以通过一些扩展机制来支持,比如允许用户自定义函数、增加配置项等。这样既能保证DSL的简洁易用,又能满足特定场景的需求。不过,这种扩展性也需要谨慎控制,防止DSL变得过于复杂。