🎆从 Prompt 到 Skill:让 Spring AI Agent 学会"装新技能"

Nacos3 AI 实战系列(2)· Spring AI Alibaba × Skill 热加载

上一篇我们让"对话"热更新了,这一篇让"技能"也能热更新。


一、凌晨两点的续集

上一篇发出去之后,评论区很多同学问了一个问题:

"Prompt 是能热更新了,但产品说想给 Agent 加个 PDF 解析工具,是不是也得发版?"

我当时回了四个字:是的,逃不掉

直到凌晨两点,我又一次被叫醒。需求很"简单":让 Agent 拥有"读取 PDF 表格并按指定格式输出"的能力。

传统流程:

  • 在 SDK 里加一个 PdfParseTool
  • 写提示词模板、调工具参数、跑测试
  • pom.xml 版本号 → 提 PR → CI → 灰度 → 全量
  • 一晚上过去了

第二天我盯着代码,脑子里浮现一个画面:

Agent 的"工具集"本质上就是一套"插件包",凭什么它不能像 npm install 一样热装?

上一篇我们让"菜谱"(Prompt)能热改了。这一篇,我们要让"整套厨具"(Skill)也能热装。改完直接下班,关灯睡觉


二、从 Prompt 到 Skill:能力跃迁

在动手前,先用一张图说清楚 Prompt 和 Skill 的区别。

Prompt 是"菜谱" :告诉模型"做什么菜"。 Skill 是"整套厨具":直接给 Agent"装备"。

维度 Prompt Skill
内容 一段文本模板 ZIP 包(SKILL.md + 工具 + 知识)
告诉模型什么 怎么说、说什么 怎么 、能什么
典型用途 改语气、加约束 加 PDF 解析、SQL 执行等"动手能力"
存储 内存 Map 远端 Nacos + 本地磁盘

方案对比

光说 Skill 好不够,我们看看市面上都有什么方案:

方案 内容形式 存储方式 集成方式 热更新
写死在代码 工具类 编译进 JAR @Resource
外部 jar 包 Java 类 Maven 仓库 改依赖
本地文件 + 定时扫描 JSON/YAML 服务器本地 自造 loader ⚠️ 分钟级
Nacos3 Skill ZIP 包 远端托管 官方 Registry 抽象 ✅ 秒级

说了这么多抽象优点,不如直接看 Nacos3 控制台长啥样:

这就是 Nacos3 自带的 Skill 管理界面:上传 ZIP 包、版本号、灰度按钮、变更历史一目了然,Agent 的"装备库"在这里是一等公民

这就是 Nacos3 的野心:它想把 Agent 的整个技能栈 都纳入配置中心管理,跟上一篇的 Prompt 一个套路。

为什么是 Nacos3 Skill? 三个理由(跟上一篇呼应):

  1. 零额外基础设施:上一篇 Nacos 已经在跑了,复用一套
  2. Skill 是"一等公民":跟 Prompt 平级,控制台直接管 ZIP 包
  3. 官方契约 :Spring AI Alibaba 的 AbstractSkillRegistry 强依赖 Nacos AI 协议,换了别的配置中心不兼容

三、架构设计:3 层拆解

老规矩,先画图再撸代码。这一篇比 Prompt 简单一点(少一个动态代理层),3 层架构

flowchart TB subgraph A[&#34;① 业务层 (Agent / Hook)&#34;] A1[&#34;registry.get('pdf-parser')<br/>listAll() → SystemPrompt&#34;] end subgraph B[&#34;② Registry 层 (NacosSkillRegistry)&#34;] B1[&#34;继承 AbstractSkillRegistry<br/>├─ 内存 Map (volatile + 同步)<br/>└─ 本地目录 /data/skills/{name}/{version}&#34;] end subgraph C[&#34;③ Sync 层 (NacosSkillSync)&#34;] C1[&#34;长轮询订阅<br/>↓ 变更触发<br/>下载 ZIP → 流式解压 → 刷新 Registry&#34;] end subgraph D[&#34;④ Nacos3 Server&#34;] D1[&#34;控制台 → Skill 管理<br/>(ZIP 上传 / 版本 / 灰度)&#34;] end A -->|get / listAll| B B <-->|reload 刷新| C C -->|subscribeSkill + downloadSkillZip| D D -.->|变更推送| C

光看架构不够直观,下面这张时序图把"启动 → 调用 → 热更新"三个阶段画在同一张图里:

sequenceDiagram autonumber participant OP as 运营人员 participant NC as Nacos 控制台 participant NS as Nacos Server participant SY as NacosSkillSync participant FS as 本地磁盘 participant RG as NacosSkillRegistry participant AG as Agent participant AI as AI 模型 rect rgb(240, 248, 255) Note over SY,RG: 【启动阶段】 SY->>NS: 建立 AiService 连接 (HTTP 模式) SY->>NS: subscribeSkill(pdf-parser) NS-->>SY: 注册长轮询监听 SY->>NS: downloadSkillZip(pdf-parser) NS-->>SY: 返回 ZIP 字节流 SY->>FS: 解压到 /data/skills/pdf-parser/latest/ SY->>RG: updateSkill(metadata) RG->>RG: 写入内存 Map + 版本缓存 end rect rgb(255, 252, 240) Note over AG,AI: 【正常运行】 AG->>RG: listAll() RG-->>AG: 返回 Skill 列表 (自动注入 system prompt) AG->>AI: 调用大模型 AG->>RG: get('pdf-parser') 按需加载 RG-->>AG: 返回 SkillMetadata end rect rgb(240, 255, 240) Note over OP,RG: 【热更新阶段】 OP->>NC: 上传新版本 PDF-Parser ZIP NC->>NS: 保存新版本 NS-->>SY: 长轮询回调 NacosSkillEvent SY->>NS: downloadSkillZip(pdf-parser) NS-->>SY: 返回新版本 ZIP SY->>FS: 清理旧目录 + 解压新版本 SY->>RG: reload() 刷新缓存 Note over RG: 下次 listAll() 自动返回新版本 end

看【热更新阶段】那块 :运营上传新版本 ZIP → Nacos 长轮询推到 Sync → 流式下载 + 解压 → 刷新 Registry → Agent 下次调用自动用上新版本。整个过程 Agent 0 感知、0 介入

4 个关键设计决策

① 为什么 Skill 用 ZIP 而不是 JSON/YAML?

Skill 不是一段文本,它是一个"包"

  • SKILL.md(核心提示词)
  • tools/*.py(工具脚本)
  • knowledge/(知识库文件)
  • resources/(二进制资源、模型文件)

单文件装不下,ZIP 才是容器标准。天然支持目录、二进制、跨平台。

② 为什么本地落盘而不是纯内存?

这是一道架构师面试题的标准答案。

  • 体积大:单个 Skill 可能几 MB 到几十 MB(特别带模型的),全放内存 GC 压力爆炸
  • 可观测:磁盘上能看到文件,运维/SRE 排查问题方便
  • 可扩展 :外部脚本能直接读 /data/skills/pdf-parser/latest/script.py

架构师思维:能用磁盘解决的事别用内存。内存是用来换速度的,不是用来堆数据的。

③ 为什么继承 AbstractSkillRegistry

Spring AI Alibaba 的 SkillsAgentHook / SkillAgent 强契约 ,必须实现 AbstractSkillRegistry 那一套接口。

不继承就用不了官方 Agent ------ 这是绕不开的硬约束

我们的 NacosSkillRegistry 继承它,免费拿到 get() / listAll() / loadFullContent() 这些方法,不用自己造。

④ 为什么用 {skillName}/{version}/ 目录结构?

bash 复制代码
/data/skills/
├── pdf-parser/
│   ├── 1.0.0/        ← 旧版本,保留
│   ├── 1.1.0/        ← 历史版本
│   └── latest/       ← 软链 or 实际目录,指向当前生效版本

latest 是约定,不是特殊处理。历史版本自然保留 → 版本回滚、A/B Test 天然支持。

这种"按目录分版本"的设计思路,跟 Linux 装软件用 /usr/local/{name}/{version} 一个路数,老而弥坚。


四、代码实战:从 0 到 1

完整代码在 scm-ai-framework/skill(地址脱敏)。这里只讲骨架和为什么

4.1 引入依赖

跟上一篇完全一样,纯净版的坑已经踩过,这里不重复:

xml 复制代码
<dependency>
    <groupId>com.alibaba.nacos</groupId>
    <artifactId>nacos-api</artifactId>
    <version>${nacos3.version}</version>
</dependency>
<dependency>
    <groupId>com.alibaba.nacos</groupId>
    <artifactId>nacos-common</artifactId>
    <version>${nacos3.version}</version>
</dependency>

4.2 配置文件

@ConfigurationProperties 全量配置,业务方零感知:

yaml 复制代码
scm:
  ai:
    skills:
      enabled: true
      server-addr: 127.0.0.1:8848
      namespace: public
      root-dir: /data/skills
      preload: false
      list:
        - pdf-parser
        - sql-executor
        - code-reviewer

几个坑提前说:

  • root-dir 留空会自动跨平台适配:Windows 走 ${java.io.tmpdir}/skills,Linux/Docker 走 /data/skills
  • 容器化部署必须把 /data/skills 挂 PV,否则容器重启数据丢
  • list 里的 Skill 名必须在 Nacos 控制台预先创建好

4.3 NacosSkillRegistry:内存 + 磁盘双层

Registry 负责对外暴露 Skill 列表,对内维护"内存 + 磁盘"双层数据:

java 复制代码
@Slf4j
public class NacosSkillRegistry extends AbstractSkillRegistry {

    @Getter
    private String skillsRootDir;

    @Builder.Default
    private SystemPromptTemplate systemPromptTemplate = SystemPromptTemplate.builder()
            .template(DEFAULT_SYSTEM_PROMPT_TEMPLATE)
            .build();

    // WHY: 版本号缓存,单独维护是为了 O(1) 查询
    private final Map<String, String> versionCache = new ConcurrentHashMap<>();

    // CRITICAL: 父类的 skills 字段是 volatile + 同步控制,写入必须走 updateSkill
    public void updateSkill(SkillMetadata metadata) {
        String version = extractVersionFromPath(metadata.getSkillPath());
        if (version != null) {
            versionCache.put(metadata.getName(), version);
        }
        skills.put(metadata.getName(), metadata);
        log.info("【NacosSkillRegistry】更新 Skill: {}, 版本: {}", metadata.getName(), version);
    }

    // WHY: 从路径偷版本号 ------ 路径 /data/skills/pdf-parser/latest 末段就是版本
    private String extractVersionFromPath(String skillPath) {
        if (skillPath == null || skillPath.isEmpty()) return null;
        try {
            return Paths.get(skillPath).getFileName().toString();
        } catch (Exception e) {
            return null;
        }
    }

    @Override
    protected void loadSkillsToRegistry() {
        if (skillsRootDir == null || skillsRootDir.isEmpty()) {
            skillsRootDir = getDefaultSkillsRootDir().toString();
        }

        versionCache.clear();
        Path rootPath = Paths.get(skillsRootDir);
        if (!Files.exists(rootPath)) {
            log.info("技能根目录不存在: {}", skillsRootDir);
            return;
        }

        // CRITICAL: 扫描目录树 {skillName}/{version}/SKILL.md
        try (var skillDirs = Files.list(rootPath)) {
            skillDirs.filter(Files::isDirectory).forEach(skillDir -> {
                String skillName = skillDir.getFileName().toString();
                try (var versionDirs = Files.list(skillDir)) {
                    versionDirs.filter(Files::isDirectory).forEach(versionDir -> {
                        var skillMdPath = versionDir.resolve("SKILL.md");
                        String description = readDescription(skillMdPath);

                        SkillMetadata metadata = SkillMetadata.builder()
                                .name(skillName)
                                .description(description)
                                .skillPath(versionDir.toString())
                                .source("nacos")
                                .build();
                        skills.put(skillName, metadata);
                        versionCache.put(skillName, versionDir.getFileName().toString());
                    });
                }
            });
        }
    }
}

架构师要点:

  • 父类 skills 字段是 volatile + 同步的 ,我们只能在 updateSkill 里写,不能直接 put
  • extractVersionFromPath 是个小巧思:路径末段天然就是版本号,省掉单独存版本字段
  • descriptionSKILL.md 的 frontmatter 里抠出来 (YAML 头部 --- 之间的内容),用于 Agent 决策

4.4 NacosSkillSync:下载 + 订阅

这是整套方案的发动机------长轮询 + ZIP 下载 + 流式解压三连击:

java 复制代码
@Slf4j
public class NacosSkillSync {

    private final NacosSkillSyncProperties properties;
    private final NacosSkillRegistry registryWrapper;
    private AiService aiService;

    // WHY: 幂等控制,同一个 Skill 不会被重复订阅
    private final Map<String, String> subscribedSkills = new ConcurrentHashMap<>();

    @PostConstruct
    public void init() {
        try {
            this.aiService = AiFactory.createAiService(buildProperties());

            // 遍历配置,逐个订阅
            if (hasSkillList()) {
                for (String skillName : properties.getList()) {
                    subscribeSkill(skillName);
                }
            }
            registryWrapper.reload();
        } catch (NacosException e) {
            throw new RuntimeException(e);
        }
    }

    private java.util.Properties buildProperties() {
        java.util.Properties props = new java.util.Properties();
        props.put(PropertyKeyConst.SERVER_ADDR, properties.getServerAddr());

        // CRITICAL: 强制 AI 模块降级使用 HTTP 传输,当前 skill 只支持 http 协议
        // WHY: Nacos 3.2.2 默认走 gRPC,但 Skill 服务只支持 HTTP,不设这个会报协议错
        props.setProperty(AiConstants.AI_TRANSPORT_MODE, "http");

        if (StrUtil.isNotBlank(properties.getNamespace())) {
            props.put(PropertyKeyConst.NAMESPACE, properties.getNamespace());
        }
        return props;
    }

    public synchronized void subscribeSkill(String skillName) {
        if (subscribedSkills.containsKey(skillName)) return;

        try {
            aiService.subscribeSkill(skillName, null, null, new AbstractNacosSkillListener() {
                @Override
                public void onEvent(NacosSkillEvent event) {
                    log.info("Skill 变更通知: name={}, version={}",
                            event.getSkillName(), event.getResolvedVersion());
                    handleSkillChanged(event.getSkillName(), event.getResolvedVersion());
                }
            });
            subscribedSkills.put(skillName, "subscribed");
        } catch (Exception e) {
            log.error("订阅 Skill 失败: {}", skillName, e);
        }
    }

    private void handleSkillChanged(String skillName, String newVersion) {
        downloadAndExtractSkill(skillName, newVersion);
        registryWrapper.reload();
    }

    // CRITICAL: 流式下载 + 解压,内存峰值只跟单个文件有关
    public void downloadAndExtractSkill(String skillName, String version) {
        try {
            byte[] zipContent = aiService.downloadSkillZip(skillName);
            if (zipContent == null || zipContent.length == 0) return;

            Path extractPath = extractZip(zipContent, skillName, version);
            refreshSingleSkillCache(skillName, extractPath);
        } catch (Exception e) {
            log.error("下载 Skill 失败: {}", skillName, e);
        }
    }

    private Path extractZip(byte[] zipContent, String skillName, String version) throws IOException {
        Path rootPath = registryWrapper.getRootPath();
        Path skillDir = rootPath.resolve(skillName);

        // WHY: 约定 "latest" 目录表示当前生效版本,覆盖前先清空
        Path extractPath = StrUtil.isBlank(version)
                ? skillDir.resolve("latest")
                : skillDir.resolve(version);

        // CRITICAL: 用 Files.walk 倒序删除(先删文件再删目录)
        if (Files.exists(extractPath)) {
            Files.walk(extractPath)
                    .sorted(Comparator.reverseOrder())
                    .forEach(p -> {
                        try { Files.delete(p); } catch (IOException ignored) {}
                    });
        }
        Files.createDirectories(extractPath);

        // 流式解压,不落中间文件
        try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipContent))) {
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                String entryName = entry.getName();
                // 去掉 ZIP 里多余的顶层目录(如 pdf-parser/)
                if (entryName.startsWith(skillName + "/")) {
                    entryName = entryName.substring(skillName.length() + 1);
                }
                if (entryName.isEmpty()) {
                    zis.closeEntry();
                    continue;
                }
                Path targetPath = extractPath.resolve(entryName);
                if (entry.isDirectory()) {
                    Files.createDirectories(targetPath);
                } else {
                    Files.createDirectories(targetPath.getParent());
                    Files.copy(zis, targetPath);
                }
                zis.closeEntry();
            }
        }
        return extractPath;
    }
}

几个架构师要点:

  • synchronized + subscribedSkills 双重防重:启动时多 Bean 并发初始化,加锁保证幂等
  • 流式解压ZipInputStream 一边读一边写,内存峰值只跟单个文件大小有关,不会把整个 ZIP 装内存
  • Files.walk 倒序删除 :先删文件再删目录,否则会抛 DirectoryNotEmptyException
  • HTTP_TRANSPORT_MODE=http 是救命的一行(生产实践会细讲)

解压完的目录长这样

SKILL.md 是 Skill 的"灵魂文件"------Agent 通过读它来决定什么时候用这个 Skill输入输出约定是什么

4.5 三个组件的装配

Registry、Sync、Properties 是三个独立的组件,需要一个 @Configuration 把它们串起来:

java 复制代码
@Configuration
@EnableConfigurationProperties(NacosSkillSyncProperties.class)
public class NacosSkillAutoConfiguration {

    @Bean
    public NacosSkillRegistry skillRegistry(NacosSkillSyncProperties properties) {
        NacosSkillRegistry registry = NacosSkillRegistry.builder()
                .skillsRootDir(properties.getRootDir())
                .build();
        registry.reload();
        return registry;
    }

    @Bean
    public NacosSkillSync skillSync(NacosSkillSyncProperties properties,
                                    NacosSkillRegistry registry) {
        return new NacosSkillSync(properties, registry);
    }
}

跟上一篇的差异:

  • 上一篇用 BeanPostProcessor 无侵入增强
  • 这一篇用显式 @Configuration ------因为 Skill 的初始化是命令式的(要主动 subscribe、主动 download),不像 Prompt 那种被动替换

4.6 在 Spring AI Alibaba Agent 中使用 Skill

这是本篇的杀手锏------光把 Skill 同步到本地没用,得让 Agent 真的能用上。

AbstractSkillRegistry 是谁?

Spring AI Alibaba 提供的官方抽象类,定义了 Agent 调用 Skill 的标准契约:

  • get(name) - 拿单个 Skill
  • listAll() - 列所有 Skill
  • readSkillContent(name) - 读 SKILL.md 全文
  • getSkillLoadInstructions() - 生成给 Agent 的提示词片段

我们的 NacosSkillRegistry 继承它,自动获得兼容性------任何 Spring AI Alibaba 的 Agent 都能直接用。

② Agent 如何"看见" Skill

Spring AI Alibaba 提供了 SkillsAgentHook,一行代码集成:

java 复制代码
@Component
public class PdfAnalystAgent {

    @Autowired
    private ChatClient chatClient;

    @Autowired
    private NacosSkillRegistry skillRegistry;

    public String analyze(String pdfPath, String userQuery) {
        // SkillsAgentHook 会在调用前自动注入 Skill 列表到 system prompt
        return chatClient.prompt()
            .system("你是一个 PDF 文档分析助手。")
            .user("PDF 路径: " + pdfPath + "\n问题: " + userQuery)
            .advisors(new SkillsAgentHook(skillRegistry))
            .call()
            .content();
    }
}

发生了什么?

  1. SkillsAgentHookskillRegistry.getSkillLoadInstructions() 拿到 Skill 列表
  2. 拼成 - Skill: pdf-parser - 解析 PDF 表格... 塞进 system prompt
  3. Agent 看到这些 Skill,根据用户输入自动选择用哪个

③ 自定义 system prompt 模板

我们覆盖了 getSystemPromptTemplate(),可以自定义 Skill 列表的展示格式

java 复制代码
@Override
public SystemPromptTemplate getSystemPromptTemplate() {
    return SystemPromptTemplate.builder()
            .template("""
                你可以使用以下技能:
                {{#each skills}}
                - 【{{name}} v{{version}}】{{description}}
                  调用方式: skill.{{name}}(input)
                {{/each}}
                """)
            .build();
}

高阶玩法:按用户角色过滤(普通用户看不到 admin 类 Skill)、按场景动态显示(金融场景才显示合规 Skill)。

最终效果 :产品新加一个 pdf-parser Skill → 上传 Nacos → Agent 下次调用自动具备 PDF 解析能力。零代码改动、零发版。


五、生产实践:架构师必看的 6 个细节

写完代码只是开始,下面这些是上线前要想的。

① Nacos 3.2.2 的 HTTP 传输坑 ⚠️(重点讲)

这是我们生产环境真金白银踩出来的坑,必须单独说。

现象

启动后 subscribeSkill() 报:

makefile 复制代码
NacosException: protocol not supported

或者更隐蔽的:控制台显示订阅成功,但永远收不到变更通知

原因

Nacos3 把 AI 模块的传输层抽象了出来,默认走 gRPC (为了性能)。但当前版本的 Skill 服务实现只支持 HTTP(Nacos 团队还在迁移),两者对不上。

解决

一行配置(NacosSkillSync.java:125):

java 复制代码
// WHY: 强制 AI 模块降级使用 HTTP 传输,当前 skill 只支持 http 协议
props.setProperty(AiConstants.AI_TRANSPORT_MODE, "http");

教训

升级 Nacos 时先在测试环境验证这个参数是否还兼容。3.2.x 升 3.3.x 时,HTTP 模式可能会被移除。

我们已经在 Nacos 社区提了 issue,等官方 Skill 服务支持 gRPC 后,这行代码就可以删了。

② 磁盘清理策略

{skillName}/{version}/ 目录结构有个副作用:旧版本永远不会被自动删除

上线 3 个月后,/data/skills/ 涨到 50GB。

解决方案

java 复制代码
@Scheduled(cron = "0 0 3 * * ?")  // 每天凌晨 3 点清理
public void cleanOldVersions() {
    // 保留 latest + 最近 2 个版本,删其他的
    for (String skillName : registryWrapper.getAllVersions().keySet()) {
        cleanOldVersionsForSkill(skillName, 2);
    }
}

③ ZIP 路径穿越风险

entry.getName() 没过滤 ../ 的话,恶意 ZIP 可以把文件写到目录外:

bash 复制代码
SKILL.md
../../../etc/passwd   ← 攻击载荷

修复方案

java 复制代码
Path targetPath = extractPath.resolve(entryName).normalize();
if (!targetPath.startsWith(extractPath)) {
    throw new IOException("Bad zip entry: " + entryName);
}

这是个老生常谈的洞,但真有不少生产事故是它引起的

④ 跨平台路径与容器化

环境 默认路径 建议
Windows 开发 ${java.io.tmpdir}/skills 默认 OK
Linux 测试 /data/skills 没问题
Docker 容器 /data/skills ⚠️ 必须挂 PV
K8s /data/skills ⚠️ 用 PVC 或者改用 /tmp/skills 接受重启丢失

SKILL.md frontmatter 解析

现在的 extractDescription() 是用 indexOf("---") 硬找的------脆且丑,YAML 里有特殊字符就崩。

建议换成 SnakeYAML

java 复制代码
Yaml yaml = new Yaml();
Map<String, Object> frontmatter = yaml.load(content);
return (String) frontmatter.get("description");

⑥ 监控告警

指标 告警阈值
/data/skills 磁盘占用 > 80%
ZIP 下载失败率 > 5%
ZIP 解压失败率 > 1%
监听器心跳超时 > 30s
subscribedSkills.size 与配置不一致 立即告警

六、写在最后

回到开头那个凌晨两点的故事。

现在产品再加需求,流程变成了:

上传 Skill ZIP → Nacos 控制台保存 → 1 秒后全网 Agent 拥有新技能 → 关灯睡觉 😴

双子篇回顾:

  • 上一篇:让"菜谱"(Prompt)热更新
  • 这一篇:让"厨具"(Skill)热装载

一个架构师最大的价值,不是写最酷的代码,而是让团队不用再为一些无聊的事熬夜。

关注不迷路


如果觉得有帮助,欢迎点赞、收藏、关注三连 👍

有任何问题欢迎评论区交流,下期见 👋

相关推荐
米小虾2 小时前
手把手教你搭建第一个生产级AI Agent:从选型到实战的完整指南
人工智能·agent
任沫2 小时前
Agent之Function Call
javascript·人工智能·go
Java_慈祥2 小时前
手把手 教你,Claude + CC-Switch 使用!!
ai编程·claude·敏捷开发
米小虾2 小时前
2026年AI Agent全面爆发:从开源生态到企业级应用的进化之路
人工智能·agent
用户6919026813392 小时前
Vibe Coding 开发项目的基本范式
人工智能·设计模式·代码规范
To_OC2 小时前
别再跟 AI 死磕 prompt 了,我写了个 Loop 让它自己改到满意为止
人工智能·aigc·agent
悟空码字2 小时前
【高德开放平台skill】从拍脑袋到看数据,我是如何把一个“选址直觉“做成 AI Skill 的
aigc·openai·ai编程
血小溅3 小时前
三大 AI 编码框架深度对比:GSD vs OpenSpec vs Superpowers
人工智能·后端
Warson_L3 小时前
什么是 PTC (Programmatic Tool Calling)?
ai编程