从零构建 Prompt Agent:一个让 AI 自动生成 AI Skill 的工程实践

你有没有遇到过这种情况——脑子里有一个模糊的需求,但写出来的 Prompt 要么太笼统、要么缺关键约束,导致 LLM 输出总是差那么一点?更进一步,如果你想为 Cursor / Claude Code 编写一个可复用的 Agent Skill(SKILL.md),光是搞懂格式规范就要翻半天文档。

Prompt Agent 就是为了解决这个问题而生的:你只需用自然语言描述任务,它就能自动分析意图、搜索匹配的 MCP 工具、生成符合规范的 SKILL.md 文件,并打包成可部署的 ZIP。同时,它还内置了一个 Prompt 优化器,能把你的"草稿级"提示词改写成专业级 Prompt。

本文将从架构设计、核心代码、关键决策三个维度,完整拆解这个项目的实现。无论你是想了解 Agent 工程化、Spring AI 实战、还是 SSE 流式交互,都能在这里找到答案。


目录

  1. 项目概览:它到底做了什么
  2. 整体架构:前后端分离 + 流式 Agent
  3. 核心模块一:Skill 生成 Agent
  4. 核心模块二:MCP 工具推荐系统
  5. 核心模块三:Prompt 优化器
  6. 前端实现:SSE 流式交互
  7. 工程化细节:验证、打包、部署
  8. 关键设计决策复盘
  9. 快速上手指南

1. 项目概览

git仓库:https://gitcode.com/forshy/prompt-agent
Prompt Agent 是一个本地化的 AI 工程平台,具备两大核心能力:

能力一:Skill Generator(技能生成器)

输入一段自然语言的任务描述,自动输出:

  • 一个符合 Cursor / Claude Agent 规范的 SKILL.md 文件
  • 自动推荐并关联相关的 MCP(Model Context Protocol)服务器
  • 可选的辅助文件(如示例脚本、参考文档)
  • 打包好的 ZIP 文件,可直接部署

适用场景:你想让 AI 帮你写一个"读取 PDF 并提取表格"的 Skill,只需描述"从 PDF 中提取表格数据并转为 CSV",系统会自动分析需求、推荐 pdf MCP 工具、生成完整的 SKILL.md。

能力二:Prompt Refine(提示词优化)

将粗糙的、口语化的提示词改写为结构化的专业 Prompt,支持 5 种风格:

风格说明适用场景
GENERIC通用专业改写日常使用
TECH_SPEC接口契约风格技术规格、API 文档
PRODUCTPRD 需求文档风格产品需求、用户故事
ACADEMIC学术严谨风格论文、研究报告
CONCISE极简风格(<200字)快速指令

技术栈一览

后端:Spring Boot 3.4 + Spring AI + Java 21
前端:Vue 3 + TypeScript + Pinia + Vite
部署:Docker Compose(Nginx + JRE 21)
LLM:兼容 OpenAI API(默认 DeepSeek,可切换任意兼容服务)

2. 整体架构

整个系统采用前后端分离架构,后端通过 SSE(Server-Sent Events) 向前端推送 Agent 的实时执行进度。

┌─────────────────────────────────────────────────────────────┐
│                        浏览器 (Vue 3)                        │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐   │
│  │ TaskInputForm │  │ AgentTrace   │  │ SkillPreview     │   │
│  │ (输入任务)     │  │ (实时日志)    │  │ (预览+下载)       │   │
│  └──────┬───────┘  └──────▲───────┘  └──────────────────┘   │
│         │                 │ SSE                              │
└─────────┼─────────────────┼─────────────────────────────────┘
          │ POST /api/skills/generate
          ▼
┌─────────────────────────────────────────────────────────────┐
│                  Spring Boot 后端                            │
│  ┌──────────────┐                                           │
│  │ SkillController│──► SseEmitter (5分钟超时)                │
│  └──────┬───────┘                                           │
│         ▼                                                   │
│  ┌──────────────────────────────────────────────┐           │
│  │ SkillAgentService  (核心编排器)                │           │
│  │  ┌─────────────────────────────────────────┐  │           │
│  │  │ 1. 调用 LLM 生成 SkillSpec              │  │           │
│  │  │ 2. LLM 可调用 searchMcpCatalog 工具     │  │           │
│  │  │ 3. SkillValidator 验证输出              │  │           │
│  │  │ 4. 失败则反馈错误,自动重试(最多2轮)    │  │           │
│  │  │ 5. SkillPackager 写入磁盘 + 打包 ZIP    │  │           │
│  │  └─────────────────────────────────────────┘  │           │
│  └──────────────────────────────────────────────┘           │
│         │                                                   │
│  ┌──────▼───────┐  ┌──────────────┐  ┌──────────────┐      │
│  │ McpCatalog   │  │ SkillValidator│  │ SkillPackager│      │
│  │ (MCP 目录)    │  │ (规范校验)    │  │ (文件打包)    │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└─────────────────────────────────────────────────────────────┘

3. 核心模块一:Skill 生成 Agent

这是整个系统的核心。Skill 生成的流程可以用一句话概括:LLM 生成结构化输出 → 校验 → 失败则自动修复 → 写入磁盘

3.1 编排器:SkillAgentService

SkillAgentService.java 是整个 Agent 的大脑。它不直接与 LLM 通信,而是通过 Spring AI 的 ChatClient 抽象来调用,这使得切换 LLM 提供商变得透明。

核心逻辑是一个带验证重试的生成循环

public Result generate(GenerateRequest request, Consumer<Event> listener) {
    listener.accept(Event.step("plan", "Analyzing the task and planning the skill..."));

    SkillSpec spec = null;
    List<String> errors = List.of();
    int rounds = 0;
    int maxRounds = properties.getMaxRepairRounds() + 1; // 默认 3 轮(1次生成 + 2次修复)

    while (rounds < maxRounds) {
        rounds++;
        // 关键:将上一轮的验证错误注入 prompt,让 LLM 自我修复
        String previousErrors = errors.isEmpty()
                ? ""
                : "# Previous validation errors\nThe last attempt failed validation:\n- "
                        + String.join("\n- ", errors)
                        + "\nFix all of them in this attempt.\n";

        // 调用 LLM,返回结构化的 SkillSpec
        spec = skillAuthorChatClient.prompt()
                .user(u -> u.text(generationTemplate)
                        .param("task", request.task())
                        .param("locationHint", request.locationHint() == null ? "" : request.locationHint())
                        .param("previousErrors", previousErrors))
                .call()
                .entity(SkillSpec.class);  // Spring AI 自动将 JSON 反序列化为 Java Record

        // 验证 LLM 输出是否符合 Cursor/Claude Skill 规范
        errors = validator.validate(spec);
        if (errors.isEmpty()) {
            listener.accept(Event.step("validate", "SkillSpec passed validation."));
            break;
        }
        // 验证失败,错误信息会被注入下一轮的 prompt
        listener.accept(Event.step("validate",
                "Validation issues (round " + rounds + "): " + String.join("; ", errors)));
    }

    // ... 打包写入磁盘 ...
}

教学要点:这里的设计体现了一个重要的 Agent 工程模式——验证-修复循环(Validate-Repair Loop)。LLM 的输出天然具有不确定性,与其期望一次成功,不如用结构化验证 + 错误反馈的方式引导它自我修正。这比简单的"重试"更聪明,因为每一轮修复都带着具体的错误信息。

3.2 结构化输出:SkillSpec

LLM 的输出不是自由文本,而是一个强类型的 JSON 对象。Spring AI 的 .entity(SkillSpec.class) 会自动完成 JSON → Java Record 的反序列化。

@JsonClassDescription("A complete Cursor/Claude Agent Skill specification ready to be written to disk.")
public record SkillSpec(
    @JsonPropertyDescription("Skill identifier. Lowercase letters, digits and hyphens only. Max 64 chars.")
    String name,

    @JsonPropertyDescription("Third-person description. Max 1024 chars.")
    String description,

    @JsonPropertyDescription("The full SKILL.md body in markdown. Do NOT include YAML frontmatter.")
    String body,

    @JsonPropertyDescription("IDs of MCP servers from the local catalog that are relevant.")
    List<String> recommendedMcpIds,

    @JsonPropertyDescription("Optional supporting files written alongside SKILL.md.")
    List<SupportingFile> supportingFiles
) {
    // compact constructor 保证不可变性
    public SkillSpec {
        recommendedMcpIds = recommendedMcpIds == null ? List.of() : List.copyOf(recommendedMcpIds);
        supportingFiles = supportingFiles == null ? List.of() : List.copyOf(supportingFiles);
    }

    public record SupportingFile(String path, String content) {}
}

教学要点@JsonClassDescription@JsonPropertyDescription 不仅是文档注解——Spring AI 会将它们注入 LLM 的 function calling schema 中,让模型"看到"每个字段的约束。这是用 Java 类型系统约束 LLM 输出的关键手段。

3.3 验证器:SkillValidator

SkillValidator.java 是保证输出质量的"守门员"。它执行一系列硬规则检查:

@Component
public class SkillValidator {

    private static final Pattern NAME_PATTERN = Pattern.compile("^[a-z0-9-]{1,64}$");
    private static final int BODY_MAX_LINES = 500;

    public List<String> validate(SkillSpec spec) {
        List<String> issues = new ArrayList<>();
        // 名称必须是小写字母+数字+连字符
        validateName(spec.name(), issues);
        // 描述必须用第三人称,不能以 "I" 或 "You" 开头
        validateDescription(spec.description(), issues);
        // 正文不能包含 YAML frontmatter(系统会自动添加)
        // 不能超过 500 行,不能有 Windows 风格路径
        validateBody(spec.body(), issues);
        // 辅助文件路径必须是相对路径,不能包含 ..
        validateSupportingFiles(spec, issues);
        return issues;  // 空列表 = 验证通过
    }

    private void validateBody(String body, List<String> issues) {
        if (body.contains("---\nname:") || body.startsWith("---")) {
            issues.add("`body` must NOT include YAML frontmatter; the system writes it.");
        }
        if (body.lines().count() > BODY_MAX_LINES) {
            issues.add("`body` exceeds 500 lines.");
        }
        if (WINDOWS_PATH.matcher(body).find()) {
            issues.add("`body` contains Windows-style paths; use forward slashes only.");
        }
    }
}

教学要点:验证器返回的是 List<String> 而非抛异常。这正是为了配合上面的"验证-修复循环"——错误信息会被直接注入下一轮 prompt,告诉 LLM “你上次哪里做错了,请修正”。


4. 核心模块二:MCP 工具推荐系统

这是项目中最有巧思的设计之一。LLM 在生成 Skill 时,不是凭空推荐 MCP 工具,而是通过 Tool Calling 实际搜索本地目录

4.1 工具定义:McpRecommendationTool

@Component
public class McpRecommendationTool {

    @Tool(description = """
            Search the local MCP server catalog by keywords. Use this whenever the user's
            task could plausibly benefit from an external tool (filesystem, GitHub, browser,
            database, search, PDF, Slack, etc). Pass space-separated keywords extracted from
            the task. Only recommend MCP servers that actually appear in the returned list.
            """)
    public List<McpEntry> searchMcpCatalog(
            @ToolParam(description = "Space-separated keywords describing the user's task domain.")
            String keywords,
            @ToolParam(description = "Maximum number of results to return.", required = false)
            Integer limit) {
        int cap = limit == null ? 5 : Math.max(1, Math.min(limit, 20));
        return catalogService.search(keywords, cap);
    }
}

教学要点@Tool 注解让这个普通 Java 方法变成了 LLM 可以调用的工具。Spring AI 会自动将方法签名、参数描述转换为 OpenAI Function Calling 的 schema。LLM 在推理过程中如果判断"这个任务可能需要外部工具",就会主动调用 searchMcpCatalog,拿到真实的搜索结果后再决定推荐哪些。

4.2 搜索算法:加权 Token 匹配

MCP 目录是一个静态 YAML 文件(17 个条目),搜索算法用了一个简洁但有效的加权 token 匹配策略:

private int scoreEntry(McpEntry entry, String[] tokens) {
    String id = lower(entry.id());
    String name = lower(entry.name());
    String desc = lower(entry.description());
    List<String> tags = entry.tags().stream().map(this::lower).toList();
    List<String> caps = entry.capabilities().stream().map(this::lower).toList();

    int score = 0;
    for (String token : tokens) {
        if (token.isBlank()) continue;
        if (id.contains(token))   score += 5;   // ID 匹配权重最高
        if (name.contains(token)) score += 4;   // 名称次之
        if (tags.stream().anyMatch(t -> t.contains(token))) score += 3;  // 标签
        if (caps.stream().anyMatch(c -> c.contains(token))) score += 3;  // 能力
        if (desc.contains(token)) score += 2;   // 描述权重最低
    }
    return score;
}

教学要点:为什么不用向量搜索?因为 MCP 目录只有 17 个条目,关键词匹配完全够用,而且零依赖、零延迟、可解释。这是工程中常见的"够用就好"原则——不要为了炫技引入不必要的复杂度。

4.3 双 ChatClient 设计

系统定义了两个独立的 ChatClient Bean,各有不同的职责:

@Configuration
public class ChatClientConfig {

    @Bean
    public ChatClient skillAuthorChatClient(ChatClient.Builder builder, McpRecommendationTool mcpTool) {
        return builder
                .defaultTools(mcpTool)  // 挂载 MCP 搜索工具
                .defaultSystem("""
                        You are a senior Skill Author Agent for Cursor / Claude Code.
                        When external tools could help, you call the `searchMcpCatalog` tool
                        to look up MCP servers by keyword and recommend only entries that the
                        tool returns. Never invent MCP servers, package names, or capabilities.
                        """)
                .build();
    }

    @Bean
    public ChatClient promptRefineChatClient(ChatClient.Builder builder) {
        return builder
                // 无工具,纯文本生成
                .defaultSystem("""
                        You are a senior prompt engineer. Your only job is to rewrite a user's
                        casual or under-specified prompt into a professional, production-grade prompt.
                        """)
                .build();
    }
}

教学要点:不要把所有职责塞进一个 LLM 调用。Skill 生成需要工具调用能力,Prompt 优化只需要纯文本生成。分开定义让每个 Client 的 system prompt 更聚焦,也避免了不需要工具的场景产生不必要的 function calling 开销。


5. 核心模块三:Prompt 优化器

Prompt 优化器的设计相对简洁,但它的 Prompt 模板值得学习。

5.1 Prompt 模板设计

You are rewriting a user's casual or under-specified prompt into a professional,
production-grade prompt. Preserve the user's original intent. Never invent business facts.

Raw user prompt (verbatim, may be in any language):
<<<RAW>>>
{rawPrompt}
<<<END RAW>>>

Target style: {style}

Style guide:
- GENERIC    : balanced professional rewrite that fits any LLM use case.
- TECH_SPEC  : interface-contract style, like an API or engineering ticket.
- PRODUCT    : PRD style. Background, user stories, success metrics, scope.
- ACADEMIC   : rigorous, citation-aware, definitions first, hedging where evidence is thin.
- CONCISE    : keep total length under ~200 characters.

# Rules
1. The rewritten prompt MUST cover: role/persona, objective, inputs, output format,
   constraints, evaluation criteria, and a slot for examples.
2. Write in the same primary language as the user's input.
3. If anything material is unclear, list it under `assumptions` rather than inventing.
4. `summary` is one sentence describing your rewrite approach.
5. `changes` is a bulleted list of the concrete improvements.

教学要点

  • 使用 <<<RAW>>> 分隔符包裹用户输入,防止 prompt 注入
  • 明确要求"不清楚的列出 assumptions"而不是自行编造,这减少了 LLM 幻觉
  • 要求输出 summarychanges,让用户能快速理解改了什么、为什么改

5.2 返回结构

public record RefinedPrompt(
    String refined,      // 改写后的 Prompt
    String summary,      // 一句话概述改写策略
    List<String> changes, // 具体改动列表
    List<String> assumptions // 不确定的假设,供用户确认
) {}

这个结构让前端可以做并排对比展示——左边原文、右边改写、下方列出改动点和假设。


6. 前端实现:SSE 流式交互

Skill 生成是一个耗时操作(可能需要 10-30 秒),如果用传统的请求-响应模式,用户会面对一个"加载中"的黑洞。项目使用 SSE(Server-Sent Events) 实现了实时进度推送。

6.1 后端 SSE 端点

@PostMapping(value = "/generate", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter generate(@RequestBody @Valid GenerateRequest request) {
    SseEmitter emitter = new SseEmitter(5 * 60 * 1000L); // 5 分钟超时

    // 异步执行,不阻塞 Servlet 线程
    taskExecutor.execute(() -> {
        try {
            service.generate(request, event -> {
                try {
                    // 每个 Event 对象被序列化为 JSON,通过 SSE 推送给前端
                    emitter.send(SseEmitter.event()
                            .name(event.type())
                            .data(objectMapper.writeValueAsString(event)));
                } catch (IOException e) {
                    emitter.completeWithError(e);
                }
            });
            emitter.complete();
        } catch (Exception e) {
            emitter.completeWithError(e);
        }
    });

    return emitter;
}

6.2 前端 SSE 解析

前端没有使用任何 SSE 库,而是直接用 ReadableStream 手动解析:

export async function generateSkillStream(
  body: { task: string; locationHint?: string; outputDirectory?: string },
  onEvent: (event: AgentEvent) => void,
  signal?: AbortSignal,
): Promise<void> {
  const response = await fetch(`${API_BASE}/skills/generate`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
    body: JSON.stringify(body),
    signal,  // AbortController 支持取消
  })

  const reader = response.body.getReader()
  const decoder = new TextDecoder('utf-8')
  let buffer = ''

  while (true) {
    const { value, done } = await reader.read()
    if (done) break
    buffer += decoder.decode(value, { stream: true })

    // SSE 协议以 \n\n 分隔消息
    const messages = buffer.split('\n\n')
    buffer = messages.pop() ?? ''  // 最后一个可能是不完整的消息,放回 buffer

    for (const raw of messages) {
      const event = parseSseMessage(raw)
      if (!event) continue
      onEvent(event)
      if (event.type === 'done' || event.type === 'error') return
    }
  }
}

教学要点:手动解析 SSE 虽然比用库麻烦,但有几个好处:

  1. 流式 buffer 处理messages.pop() 处理了 TCP 分包导致的消息不完整问题
  2. AbortController 集成:用户可以随时取消正在进行的生成
  3. 零依赖:不引入额外的 SSE 库

6.3 Pinia 状态管理

export const useSkillStore = defineStore('skill', () => {
  const status = ref<'idle' | 'running' | 'done' | 'error'>('idle')
  const events = ref<AgentEvent[]>([])  // 累积所有 SSE 事件
  const result = ref<SkillResult | null>(null)
  let abortController: AbortController | null = null

  async function generate(task: string, locationHint?: string, outputDirectory?: string) {
    status.value = 'running'
    events.value = []
    abortController = new AbortController()

    try {
      await generateSkillStream(
        { task, locationHint, outputDirectory },
        (event) => {
          events.value.push(event)  // 实时追加事件
          if (event.type === 'done') {
            result.value = event.payload as SkillResult
            status.value = 'done'
          }
        },
        abortController.signal,
      )
    } catch (e) {
      if (e instanceof DOMException && e.name === 'AbortError') {
        status.value = 'idle'
      } else {
        status.value = 'error'
      }
    }
  }

  function cancel() {
    abortController?.abort()
  }

  return { status, events, result, generate, cancel }
})

7. 工程化细节

7.1 LLM 提供商无缝切换

通过环境变量的级联 fallback 设计,一行配置就能切换 LLM:

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY:${DEEPSEEK_API_KEY:}}
      base-url: ${OPENAI_BASE_URL:${DEEPSEEK_BASE_URL:https://api.deepseek.com}}
      chat:
        options:
          model: ${OPENAI_MODEL:${DEEPSEEK_MODEL:deepseek-chat}}
# 使用 DeepSeek(默认)
DEEPSEEK_API_KEY=sk-xxx

# 切换到 OpenAI
OPENAI_API_KEY=sk-xxx
OPENAI_BASE_URL=https://api.openai.com
OPENAI_MODEL=gpt-4o

# 切换到任意 OpenAI 兼容服务
OPENAI_API_KEY=xxx
OPENAI_BASE_URL=https://your-provider.com/v1
OPENAI_MODEL=your-model

7.2 无数据库设计

整个系统没有数据库。MCP 目录是静态 YAML 文件,生成的 Skill 直接写入文件系统。这带来了:

  • 零运维成本:不需要维护数据库
  • 可移植性workspace/ 目录就是一个完整的数据快照
  • 简单性:数据结构简单,YAML + 文件系统完全够用

7.3 Docker 一键部署

# docker-compose.yml
services:
  backend:
    build:
      context: .
      dockerfile: Dockerfile.backend  # Maven 多阶段构建 → JRE 21
    env_file: .env
    volumes:
      - workspace-data:/app/workspace  # 持久化生成的 Skill

  frontend:
    build:
      context: .
      dockerfile: Dockerfile.frontend  # Node 构建 → Nginx 静态服务
    ports:
      - "80:80"
    depends_on:
      - backend

Nginx 配置中特别处理了 SSE 的代理——必须禁用缓冲才能实时推送事件:

location /api/ {
    proxy_pass http://backend:8080;
    proxy_buffering off;           # SSE 必须禁用缓冲
    proxy_cache off;
    chunked_transfer_encoding off;
}

8. 关键设计决策复盘

决策一:结构化输出 + 验证循环,而非自由文本

选择:让 LLM 返回强类型 JSON(SkillSpec),用代码验证,失败则带错误信息重试。

为什么不选自由文本 + 正则解析:自由文本解析脆弱,格式稍有变化就会失败。结构化输出利用了 LLM 的 function calling 能力,输出稳定性高得多。

决策二:Tool Calling 而非 RAG

选择:LLM 通过 @Tool 注解的方法搜索 MCP 目录,而非把整个目录塞进 prompt。

为什么不用 RAG:MCP 目录只有 17 个条目,Tool Calling 的额外开销(一次 API 调用)换来的是更精准的推荐和更小的 prompt 体积。而且 Tool Calling 的结果是结构化的,LLM 可以直接引用返回的 McpEntry 对象。

决策三:SSE 而非 WebSocket

选择:单向的 Server-Sent Events,而非全双工 WebSocket。

为什么不用 WebSocket:Skill 生成是"服务端推送、客户端接收"的单向流,SSE 天然适合。WebSocket 的全双工能力在这里用不到,反而增加了连接管理的复杂度。SSE 还有一个隐藏优势:浏览器原生支持自动重连。

决策四:文件系统而非数据库

选择:Skill 产物写入文件,MCP 目录用 YAML 文件。

为什么不用数据库:Skill 的核心产物是 Markdown 文件,存数据库再导出反而是多此一举。文件系统的目录结构天然就是 Skill 的组织方式(workspace/skills/{uuid}/{name}/SKILL.md)。


9. 快速上手指南

环境要求

  • Java 21+
  • Node.js 20+
  • Maven 3.9+
  • DeepSeek API Key(或任意 OpenAI 兼容的 API Key)

本地开发

# 1. 克隆项目
git clone <your-repo-url>
cd promptAgent

# 2. 配置 API Key
cp .env.example .env
# 编辑 .env,填入你的 DEEPSEEK_API_KEY

# 3. 启动后端
cd backend
mvn spring-boot:run
# 后端运行在 http://localhost:8080

# 4. 启动前端(新终端)
cd frontend
npm install
npm run dev
# 前端运行在 http://localhost:5173

Docker 部署

cp .env.example .env
# 编辑 .env,填入 API Key
docker compose up -d --build
# 访问 http://localhost

使用示例

Skill 生成:在文本框输入"从 PDF 文件中提取表格数据并导出为 CSV",点击生成,等待 Agent 完成后下载 ZIP。

Prompt 优化:输入一段粗糙的提示词,选择风格(如 TECH_SPEC),获取结构化的专业 Prompt。


总结

Prompt Agent 的核心理念是:用 AI 来构建 AI 的工具

它展示了几个重要的 Agent 工程实践:

  1. 结构化输出 + 验证循环:比自由文本更可靠,比纯重试更智能
  2. Tool Calling 增强:让 LLM 能访问外部知识,而不是凭空编造
  3. 流式交互:用 SSE 让用户看到 Agent 的"思考过程"
  4. 关注点分离:不同的 LLM 任务用不同的 ChatClient,各自优化

这个项目不依赖任何数据库或向量存储,却实现了完整的 Agent 工程链路。它证明了一件事:好的工程设计不在于用了多少技术,而在于每个技术用对了位置。


项目作者:songhaoyu | 技术栈:Spring Boot 3.4 + Spring AI + Vue 3 + TypeScript

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐