普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月22日技术

每日一题-移除最小数对使数组有序 I🟢

2026年1月22日 00:00

给你一个数组 nums,你可以执行以下操作任意次数:

  • 选择 相邻 元素对中 和最小 的一对。如果存在多个这样的对,选择最左边的一个。
  • 用它们的和替换这对元素。

返回将数组变为 非递减 所需的 最小操作次数 

如果一个数组中每个元素都大于或等于它前一个元素(如果存在的话),则称该数组为非递减

 

示例 1:

输入: nums = [5,2,3,1]

输出: 2

解释:

  • 元素对 (3,1) 的和最小,为 4。替换后 nums = [5,2,4]
  • 元素对 (2,4) 的和为 6。替换后 nums = [5,6]

数组 nums 在两次操作后变为非递减。

示例 2:

输入: nums = [1,2,2]

输出: 0

解释:

数组 nums 已经是非递减的。

 

提示:

  • 1 <= nums.length <= 50
  • -1000 <= nums[i] <= 1000

3507. 移除最小数对使数组有序 I

作者 stormsunshine
2025年4月7日 06:11

解法一

思路和算法

如果初始时数组 $\textit{nums}$ 已经非递减,则最小操作次数是 $0$。以下只考虑初始时数组 $\textit{nums}$ 不满足非递减的情况。

最直观的思路是模拟数组的操作。每次操作时,遍历数组 $\textit{nums}$ 寻找相邻元素对中的元素和最小且最左边的一个元素对,用元素和替换这对元素,直到数组变成非递减,此时的操作次数即为将数组变为非递减所需的最小操作次数。

用 $n$ 表示数组 $\textit{nums}$ 的长度。由于每次操作之后都会将数组中的元素个数减少 $1$,因此每次操作应将执行合并操作的元素对右侧的元素向左移动。对于 $0 \le k < n$ 的整数 $k$,在执行 $k$ 次操作之后,剩余元素个数是 $n - k$,因此下一次操作时应只考虑数组的前 $n - k$ 个元素。

代码

###Java

class Solution {
    public int minimumPairRemoval(int[] nums) {
        int removals = 0;
        int n = nums.length;
        while (!isNonDecreasing(nums, n)) {
            update(nums, n);
            removals++;
            n--;
        }
        return removals;
    }

    public void update(int[] nums, int n) {
        int minSum = Integer.MAX_VALUE;
        int index = -1;
        for (int i = 0; i < n - 1; i++) {
            if (nums[i] + nums[i + 1] < minSum) {
                minSum = nums[i] + nums[i + 1];
                index = i;
            }
        }
        if (index >= 0) {
            nums[index] = minSum;
            for (int i = index + 1; i < n - 1; i++) {
                nums[i] = nums[i + 1];
            }
        }
    }

    public boolean isNonDecreasing(int[] nums, int n) {
        for (int i = 1; i < n; i++) {
            if (nums[i - 1] > nums[i]) {
                return false;
            }
        }
        return true;
    }
}

###C#

public class Solution {
    public int MinimumPairRemoval(int[] nums) {
        int removals = 0;
        int n = nums.Length;
        while (!IsNonDecreasing(nums, n)) {
            Update(nums, n);
            removals++;
            n--;
        }
        return removals;
    }

    public void Update(int[] nums, int n) {
        int minSum = int.MaxValue;
        int index = -1;
        for (int i = 0; i < n - 1; i++) {
            if (nums[i] + nums[i + 1] < minSum) {
                minSum = nums[i] + nums[i + 1];
                index = i;
            }
        }
        if (index >= 0) {
            nums[index] = minSum;
            for (int i = index + 1; i < n - 1; i++) {
                nums[i] = nums[i + 1];
            }
        }
    }

    public bool IsNonDecreasing(int[] nums, int n) {
        for (int i = 1; i < n; i++) {
            if (nums[i - 1] > nums[i]) {
                return false;
            }
        }
        return true;
    }
}

复杂度分析

  • 时间复杂度:$O(n^2)$,其中 $n$ 是数组 $\textit{nums}$ 的长度。最差情况需要执行 $n - 1$ 次操作,每次操作的时间是 $O(n)$,因此时间复杂度是 $O(n^2)$。

  • 空间复杂度:$O(1)$。所有的操作均为原地修改数组。

解法二

思路和算法

使用双向链表、哈希表和优先队列,可以将时间复杂度降低到 $O(n \log n)$。具体见题解「3510. 移除最小数对使数组有序 II」。

代码

###Java

class Solution {
    private class Node {
        private int index;
        private long value;
        private Node prev;
        private Node next;

        public Node() {
            this(-1, Integer.MAX_VALUE);
        }

        public Node(int index, long value) {
            this.index = index;
            this.value = value;
            prev = null;
            next = null;
        }

        public int getIndex() {
            return index;
        }

        public long getValue() {
            return value;
        }

        public void setValue(long value) {
            this.value = value;
        }

        public Node getPrev() {
            return prev;
        }

        public void setPrev(Node prev) {
            this.prev = prev;
        }

        public Node getNext() {
            return next;
        }

        public void setNext(Node next) {
            this.next = next;
        }
    }

    public int minimumPairRemoval(int[] nums) {
        int removals = 0;
        Map<Integer, Node> indexToNode = new HashMap<Integer, Node>();
        Node pseudoHead = new Node();
        Node pseudoTail = new Node();
        Node prevNode = pseudoHead;
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            Node node = new Node(i, nums[i]);
            indexToNode.put(i, node);
            prevNode.setNext(node);
            node.setPrev(prevNode);
            prevNode = node;
        }
        prevNode.setNext(pseudoTail);
        pseudoTail.setPrev(prevNode);
        PriorityQueue<long[]> pq = new PriorityQueue<long[]>((a, b) -> a[0] != b[0] ? Long.compare(a[0], b[0]) : Long.compare(a[1], b[1]));
        int reversePairs = 0;
        for (int i = 0; i < n - 1; i++) {
            pq.offer(new long[]{nums[i] + nums[i + 1], i, i + 1});
            if (nums[i] > nums[i + 1]) {
                reversePairs++;
            }
        }
        while (reversePairs > 0) {
            long[] arr = pq.poll();
            long newValue = arr[0];
            int index1 = (int) arr[1], index2 = (int) arr[2];
            if (!indexToNode.containsKey(index1) || !indexToNode.containsKey(index2) || newValue != indexToNode.get(index1).getValue() + indexToNode.get(index2).getValue()) {
                continue;
            }
            removals++;
            Node node1 = indexToNode.get(index1), node2 = indexToNode.get(index2);
            if (node1.getValue() > node2.getValue()) {
                reversePairs--;
            }
            if (node1.getPrev().getIndex() >= 0 && node1.getPrev().getValue() > node1.getValue()) {
                reversePairs--;
            }
            if (node2.getNext().getIndex() >= 0 && node2.getNext().getValue() < node2.getValue()) {
                reversePairs--;
            }
            node1.setValue(newValue);
            remove(node2);
            indexToNode.remove(index2);
            if (node1.getPrev().getIndex() >= 0) {
                pq.offer(new long[]{node1.getPrev().getValue() + node1.getValue(), node1.getPrev().getIndex(), node1.getIndex()});
                if (node1.getPrev().getValue() > node1.getValue()) {
                    reversePairs++;
                }
            }
            if (node1.getNext().getIndex() >= 0) {
                pq.offer(new long[]{node1.getValue() + node1.getNext().getValue(), node1.getIndex(), node1.getNext().getIndex()});
                if (node1.getNext().getValue() < node1.getValue()) {
                    reversePairs++;
                }
            }
        }
        return removals;
    }

    public void remove(Node node) {
        Node prev = node.getPrev(), next = node.getNext();
        prev.setNext(next);
        next.setPrev(prev);
    }
}

###C#

public class Solution {
    public class Node {
        public int Index { get; set; }
        public long Value { get; set; }
        public Node Prev { get; set; }
        public Node Next { get; set; }

        public Node() : this(-1, int.MaxValue) {

        }

        public Node(int index, long value) {
            Index = index;
            Value = value;
            Prev = null;
            Next = null;
        }
    }

    public int MinimumPairRemoval(int[] nums) {
        int removals = 0;
        IDictionary<int, Node> indexToNode = new Dictionary<int, Node>();
        Node pseudoHead = new Node();
        Node pseudoTail = new Node();
        Node prevNode = pseudoHead;
        int n = nums.Length;
        for (int i = 0; i < n; i++) {
            Node node = new Node(i, nums[i]);
            indexToNode.Add(i, node);
            prevNode.Next = node;
            node.Prev = prevNode;
            prevNode = node;
        }
        prevNode.Next = pseudoTail;
        pseudoTail.Prev = prevNode;
        Comparer<Tuple<long, int, int>> comparer = Comparer<Tuple<long, int, int>>.Create((a, b) => a.Item1 != b.Item1 ? a.Item1.CompareTo(b.Item1) : a.Item2.CompareTo(b.Item2));
        PriorityQueue<Tuple<long, int, int>, Tuple<long, int, int>> pq = new PriorityQueue<Tuple<long, int, int>, Tuple<long, int, int>>(comparer);
        int reversePairs = 0;
        for (int i = 0; i < n - 1; i++) {
            pq.Enqueue(new Tuple<long, int, int>(nums[i] + nums[i + 1], i, i + 1), new Tuple<long, int, int>(nums[i] + nums[i + 1], i, i + 1));
            if (nums[i] > nums[i + 1]) {
                reversePairs++;
            }
        }
        while (reversePairs > 0) {
            Tuple<long, int, int> tuple = pq.Dequeue();
            long newValue = tuple.Item1;
            int index1 = tuple.Item2, index2 = tuple.Item3;
            if (!indexToNode.ContainsKey(index1) || !indexToNode.ContainsKey(index2) || newValue != indexToNode[index1].Value + indexToNode[index2].Value) {
                continue;
            }
            removals++;
            Node node1 = indexToNode[index1], node2 = indexToNode[index2];
            if (node1.Value > node2.Value) {
                reversePairs--;
            }
            if (node1.Prev.Index >= 0 && node1.Prev.Value > node1.Value) {
                reversePairs--;
            }
            if (node2.Next.Index >= 0 && node2.Next.Value < node2.Value) {
                reversePairs--;
            }
            node1.Value = newValue;
            Remove(node2);
            indexToNode.Remove(index2);
            if (node1.Prev.Index >= 0) {
                pq.Enqueue(new Tuple<long, int, int>(node1.Prev.Value + node1.Value, node1.Prev.Index, node1.Index), new Tuple<long, int, int>(node1.Prev.Value + node1.Value, node1.Prev.Index, node1.Index));
                if (node1.Prev.Value > node1.Value) {
                    reversePairs++;
                }
            }
            if (node1.Next.Index >= 0) {
                pq.Enqueue(new Tuple<long, int, int>(node1.Value + node1.Next.Value, node1.Index, node1.Next.Index), new Tuple<long, int, int>(node1.Value + node1.Next.Value, node1.Index, node1.Next.Index));
                if (node1.Next.Value < node1.Value) {
                    reversePairs++;
                }
            }
        }
        return removals;
    }

    public void Remove(Node node) {
        Node prev = node.Prev, next = node.Next;
        prev.Next = next;
        next.Prev = prev;
    }
}

复杂度分析

  • 时间复杂度:$O(n \log n)$,其中 $n$ 是数组 $\textit{nums}$ 的长度。遍历数组初始化数据结构的时间是 $O(n \log n)$,最差情况需要执行 $n - 1$ 次操作,每次操作中的双向链表和哈希表的更新时间是 $O(1)$,优先队列的更新时间是 $O(\log n)$,因此时间复杂度是 $O(n \log n)$。

  • 空间复杂度:$O(n)$,其中 $n$ 是数组 $\textit{nums}$ 的长度。双向链表、哈希表和优先队列的空间是 $O(n)$。

昨天 — 2026年1月21日技术

手把手实现链表:单链表与双链表的完整实现

作者 颜酱
2026年1月21日 18:26
手把手实现链表:单链表与双链表的完整实现 链表是数据结构的基础,也是面试高频考点。很多初学者会卡在“指针操作混乱”“边界条件处理不当”等问题上。本文将从设计思路出发,拆解单链表实现的核心逻辑,同时补充

大文档也能丝滑流式渲染?试试Markstream-Vue,体验前所未有的Markdown流畅感!

作者 Simon_He
2026年1月21日 18:13

为什么选择Markstream-Vue?只因它“流”得够快!

  • 🚀 真·流式渲染,支持超大文档、实时预览,边写边看不卡顿
  • 🏆 彻底解决传统Markdown组件渲染慢、卡顿、内存暴涨等痛点
  • 🧩 组件化设计,Vue 3项目即插即用,API极简
  • 🎨 支持代码高亮、公式、流程图等丰富内容,体验无缝流畅
  • 🔥 SSR/静态站点/移动端全兼容,性能拉满

真实场景,极致体验

  • 技术文档、知识库、长篇小说、实时协作编辑器……再大的内容都能流畅渲染
  • 支持内容“边加载边渲染”,让用户体验“所见即所得”的极致流畅

3步上手,流式体验立享

  1. 安装依赖
pnpm add markstream-vue
  1. 引入组件
<Markstream :source="longMarkdown" stream />
  1. 享受流式渲染的丝滑体验!

你的Star,是我持续优化的最大动力!

👉 GitHub地址


一杯茶时间,带你用 RWKV 并发模型做 VS Code 多候选代码补全插件 🤔🤔🤔

作者 Moment
2026年1月21日 18:05

在写这份实现教程之前,我已经把该插件的一个版本发布到了 VS Code 扩展市场,在市场中搜索 rwkv 即可找到,你可以先安装试用,再决定是否跟着下文从零实现一版。

本文以这款基于 RWKV 模型的智能代码补全插件为例,讲解从零实现 VS Code 扩展的思路与步骤,并说明如何接入 rwkv_lightning 后端。

该插件通过并发推理一次返回多个不同的补全答案供选择,在侧边栏展示,方便在多种写法之间对比、挑选后再插入,适合写一半、让模型多想几种实现的编码方式;光标后有代码时自动走 FIM(Fill-in-the-Middle)接口做中间填充,否则走普通续写。全文按功能目标、代码实现(项目结构、补全触发、API 调用、Webview 展示与插入)、后端接入组织,后端部分包含硬件要求、模型准备、与 Albatross 的关系、启动服务、模型加载机制、HTTP API、快速测试以及插件配置与验证,文末附常见问题。

下图为在编辑器中触发补全后,并发推理得到的多个不同答案在侧边栏展示、点击即可插入到光标位置的情形。

rwkv-code-completion 效果

前端项目地址:rwkv-code-completion

后端项目地址:rwkv_lightning

一、我们要做怎样的功能

动手写代码之前,首先要考虑我们要实现一个什么样的 VS Code 插件,这决定了后续的架构与实现方式。

在本例中,我们想做一款智能代码补全插件,并事先想清楚四件事。补全结果通过并发推理一次返回多个不同的答案,在侧边栏展示供用户选择,点选后插入。根据光标后是否已有代码,在 FIM(Fill-in-the-Middle)与普通续写之间自动切换接口。在空格、换行、删除等操作时自动触发,并做好防抖与取消,避免频繁请求。服务地址、密码、生成长度、采样参数(temperaturetop_p)、候选数量、防抖延迟等通过 VS Code 设置暴露。把这四件事的对应关系梳理出来,大致如下:

20260121175047

把这些想清楚之后,再按代码实现过程和如何接入后端两部分往下做。

二、代码实现过程

2.1 项目结构

yo code 或手工 scaffold 一个扩展后,核心只需两个源码文件,职责分开,与 VS Code 打交道的放一边,与后端 HTTP 打交道的放另一边,方便维护和单测。

  • src/extension.ts 作为插件入口,在 activate 里实现 CompletionItemProvider、注册补全、用 onDidChangeTextDocument 监听编辑并按条件触发补全;拿到候选列表后,不再往原生 suggest 里塞,而是创建 Webview、渲染多条结果,并处理用户点击插入与插完再补全。
  • src/completionService.ts 负责补全服务,根据有无 suffix 选择调用普通续写接口或 FIM 接口,组装请求体、发 fetch、解析 data.choicesstring[],并透传 AbortSignal 以支持取消。

两者与后端的关系可以概括为:

20260121175344

package.json 里,main 指向打包后的入口(如 ./dist/extension.js),VS Code 按它加载扩展;activationEvents 可设为 onStartupFinished,这样只在 IDE 就绪后才激活,避免启动时卡顿;contributes.configuration 声明 enabledbaseUrlpasswordmaxTokenstemperaturetopPnumChoicesdebounceDelay 等,用户改设置后可通过 vscode.workspace.getConfiguration("rwkv-code-completion") 读到。

构建可用 esbuild 或 tsc,把 extension.ts 等打出到 dist,调试和发布都从 dist 走。

2.2 激活与补全触发

激活时在 activate(context) 里完成两件事,一是向 VS Code 注册谁在什么情况下提供补全,二是监听文档变更,在特定编辑动作后自动调出补全,用户不必每次手动按 Ctrl+Space。

实现 vscode.CompletionItemProviderprovideCompletionItems(document, position, token, context),再用 vscode.languages.registerCompletionItemProvider 挂上去。selector{ pattern: "**" } 表示对所有语言生效;第三参数 triggerChars 是一串字符,当用户输入或删除其中某一个时,VS Code 会来调 provideCompletionItems。这里把空格、换行以及 ASCII 33–126(常见可打印字符)都放进去了,这样在写代码、加空格、换行时都有机会触发,例如:

const selector = { pattern: "**" };
const triggerChars = [
  " ",
  "\n",
  ...Array.from({ length: 94 }, (_, i) => String.fromCharCode(i + 33)),
];
vscode.languages.registerCompletionItemProvider(
  selector,
  provider,
  ...triggerChars,
);

光有 triggerChars 还不够,例如用户输入 abc 时也会触发,容易导致敲一个字母就发一次请求。因此再加一层文档变更的过滤,用 vscode.workspace.onDidChangeTextDocument 监听,只有在本次编辑是删除、换行或输入一个空格时,才在防抖后执行 editor.action.triggerSuggest,从而间接调用 provideCompletionItems。这样可以把触发收敛到更自然的断句、换行场景,例如:

const shouldTrigger = event.contentChanges.some((change) => {
  const isDelete = change.rangeLength > 0 && change.text === "";
  const isNewline = change.text === "\n" || change.text === "\r\n";
  const isSpace = change.text === " ";
  return isDelete || isNewline || isSpace;
});
if (shouldTrigger) {
  debounceTimer = setTimeout(() => {
    vscode.commands.executeCommand("editor.action.triggerSuggest");
  }, config.debounceDelay);
}

防抖时间用 config.debounceDelay(如 150–300ms),用户停一会儿才发请求,减少连打时的无效调用。还可以加两条限制,一是只处理当前活动编辑器的文档,避免在切文件、分屏时误触发,二是与上一次触发至少间隔几百毫秒,进一步避免短时间内重复弹补全。整体触发链路如下:

20260121175403

2.3 补全逻辑与 API 调用

provideCompletionItems 被调用后,先做一轮要不要真的发请求的过滤和节流,再取上下文、调后端、拿 string[]

流程可以拆成五步。一,读配置,若 enabled 为 false 直接 return null。二,防抖,用 setTimeout(..., debounceDelay) 把实际请求放到回调里;若在等待期间又有新的触发,则 clearTimeout 掉上一次,只保留最后一次,这样连续输入时只会发一次请求。三,若此前已有进行中的 fetch,用 AbortController.abort() 取消,再 new AbortController() 给本次请求用。四,取上下文,前缀 prefix 为从文档开头到光标前的文本,document.getText(new vscode.Range(0, 0, position)),过长时截断到约 2000 字符,避免超过后端限制;后缀 suffix 为从光标到往后若干行(如 10 行),主要用来判断光标后是否还有代码,从而决定走 FIM 还是普通续写。五,调用 CompletionService.getCompletion(prefix, suffix, languageId, config, abortController.signal),在 withProgress 里展示正在生成 N 个补全并可取消。五步关系如下:

20260121175421

CompletionService.getCompletion 内部按 suffix 是否非空分支,有后缀则认为用户在中间写代码,走 FIM,否则走普通续写。接口选择如下:

20260121175704

例如下面这样。

async getCompletion(prefix, suffix, languageId, config, signal): Promise<string[]> {
  const hasSuffix = suffix && suffix.trim().length > 0;
  return hasSuffix
    ? this.callFIMAPI(prefix, suffix, config, signal)
    : this.callCompletionAPI(prefix, config, signal);
}

普通补全走 callCompletionAPI,请求 POST {baseUrl}/v2/chat/completions。body 里 contentsArray(numChoices).fill(prefix),即同一段 prefix 复制多份,利用后端批量接口一次推理出多条不同采样结果;再配上 stream: falsepasswordmax_tokenstemperaturetop_pstop_tokens 等。返回的 data.choices 里,每条取 choice.message?.content || choice.text,trim 掉首尾空白并滤掉空串,得到 string[]

FIM 补全走 callFIMAPI,请求 POST {baseUrl}/FIM/v1/batch-FIMprefixsuffix 各为长度为 4 的数组(同一 prefix、同一 suffix 各复制 4 份),对应 4 条并发中间填充;其它参数与普通补全类似,解析方式相同。两处都把 signal 传给 fetch,这样在用户点击取消、或防抖导致下一次触发而 abort() 时,正在进行的请求会被中断,不把过时结果再展示出来。

2.4 Webview 展示与插入

拿到 string[] 之后,不转成 CompletionItem[] 通过 resolve(items) 塞给原生 suggest,因为原生列表单条、偏短,且没法做多列、点击选一等自定义交互。这里改为 resolve(null) 表示不往建议列表里填,同时在 withProgress 里调 showCompletionWebview(document, position, completions, languageId),用 Webview 在侧边栏展示多条候选,支持多选一、点即插、插完再补。

vscode.window.createWebviewPanel 创建 Webview,指定 id、标题、ViewColumn.Two 在侧边打开,以及 enableScripts: trueretainContextWhenHidden: true 以便跑脚本和在切走时保留状态。panel.webview.htmlgetWebviewContent(completions, languageId) 生成。在打开面板之前,必须把当时的 documentposition 存到闭包或变量里,因为 Webview 是异步的,用户可能切文件、移光标,等到点击插入时要以当初触发补全的那次位置为准,否则会插错地方。

const panel = vscode.window.createWebviewPanel(
  "rwkvCompletion",
  "RWKV 代码补全 (N 个选项)",
  vscode.ViewColumn.Two,
  { enableScripts: true, retainContextWhenHidden: true },
);
panel.webview.html = getWebviewContent(completions, languageId);

HTML 里顶部放标题与简短说明,下面一个 div 容器,用 grid-template-columns: 1fr 1fr 做多列布局,每个格子一个 div.code-block,含小标题(序号、字符数、行数)和 <pre><code> 放补全内容。补全文本要先做 HTML 转义再插入,避免 XSS;颜色、背景用 var(--vscode-editor-background) 等,跟主题一致;:hover.selected 给一点高亮,点的时候有反馈。

前端通过 acquireVsCodeApi() 拿到和扩展通信的 API,completionsgetWebviewContent 里用 JSON 注入到页面。每个 code-block 点击时执行 vscode.postMessage({ command: 'insert', code: completions[index] })。扩展侧在 panel.webview.onDidReceiveMessage 里监听,若 message.command === 'insert',先 vscode.window.showTextDocument(targetDocument, ViewColumn.One) 把原文档激活到主编辑区,再用 editor.edit(eb => eb.insert(targetPosition, message.code)) 在事先存好的 targetPosition 插入;插入成功后 panel.dispose() 关掉 Webview,并 setTimeout(..., 300) 后执行 editor.action.triggerSuggest,让光标后的新内容再触发一轮补全,形成补全、选一、再补全的连贯体验。从拿到结果到插入再触发的流程如下:

20260121175751

原生 suggest 只能一条条、样式固定,没法同时展示多条并发结果和自定义交互;用 Webview 可以自己布局、自己处理点击和插入,更适合并发推理、多答案选一的用法。

三、如何接入后端

插件通过 HTTP 调用 rwkv_lightning,需要先部署后端,再在 VS Code 里填好配置。扩展详情页会标注后端部署与配置说明,便于快速上手,下图为扩展市场中的页面示意。

RWKV 代码补全 - 扩展市场页面

接入后端的整体步骤如下。

20260121175818

3.1 硬件要求

重要提示:本后端必须使用 GPU 加速,不支持纯 CPU 运行。

rwkv_lightning 依赖自定义的 CUDA 或 HIP 内核进行高性能推理,因此需要以下硬件之一:

  • NVIDIA GPU:需要支持 CUDA 的 NVIDIA 显卡,并安装 CUDA 工具包
  • AMD GPU:需要支持 ROCm 的 AMD 显卡,并安装 ROCm 运行时

如果您只有 CPU 环境,请使用 llama.cpp 进行 RWKV 模型的 CPU 推理,该项目针对 CPU 进行了专门优化。

3.2 模型文件准备

rwkv_lightning 当前不提供自动下载功能,需要您自行准备模型权重文件。

下载模型权重

RWKV-7 模型的官方权重托管在 Hugging Face 上,推荐从 BlinkDL/rwkv7-g1 仓库下载。模型文件格式为 .pth,例如 rwkv7-g1b-1.5b-20251202-ctx8192.pth

您可以通过以下方式下载:

方式一:使用 huggingface-cli(推荐)

# 首先登录 Hugging Face(如未登录)
huggingface-cli login

# 下载模型文件
huggingface-cli download BlinkDL/rwkv7-g1 \
  rwkv7-g1b-1.5b-20251202-ctx8192.pth \
  --local-dir /path/to/models \
  --local-dir-use-symlinks False

方式二:使用 Python 脚本

from huggingface_hub import hf_hub_download

model_path = hf_hub_download(
    repo_id="BlinkDL/rwkv7-g1",
    filename="rwkv7-g1b-1.5b-20251202-ctx8192.pth",
    local_dir="/path/to/models"
)
print(f"模型已下载到: {model_path}")

路径命名规则

启动服务时,--model-path 支持两种写法。写法一:不带后缀,程序会自动补上 .pth,例如:

--model-path /path/to/rwkv7-g1b-1.5b-20251202-ctx8192
# 实际加载: /path/to/rwkv7-g1b-1.5b-20251202-ctx8192.pth

3.3 与 Albatross 的关系

rwkv_lightning 是基于 Albatross 高效推理引擎开发的 HTTP 服务后端。Albatross 是 BlinkDL 开发的高性能 RWKV 推理引擎,专注于底层计算优化和性能基准测试。

Albatross 项目简介

Albatross 是一个独立的开源项目,GitHub 地址:github.com/BlinkDL/Alb… RWKV-7 模型的高效推理实现,包括:

  • 批量推理支持:支持大规模批量处理,在 RTX 5090 上可实现 7B 模型 fp16 bsz960 超过 10000 token/s 的解码速度
  • 性能优化:集成了 CUDA Graph、稀疏 FFN、自定义 CUDA 内核等优化技术
  • 基准测试工具:提供详细的性能基准测试脚本,用于评估不同配置下的推理性能
  • 参考实现:包含完整的模型实现和工具类,可作为开发参考

性能参考数据

根据 Albatross 官方测试结果(RTX 5090,RWKV-7 7.2B fp16):

  • 单样本解码(bsz=1):145+ token/s,使用 CUDA Graph 优化后可达 123+ token/s
  • 批量解码(bsz=960):10250+ token/s
  • Prefill 阶段(bsz=1):11289 token/s
  • 批量解码(bsz=320):5848 token/s,速度恒定且显存占用稳定(RNN 特性)

rwkv_lightning 的定位

rwkv_lightning 在 Albatross 的基础上,专注于提供生产级的 HTTP 推理服务:

  • HTTP API 接口:提供完整的 RESTful API,支持流式和非流式推理
  • 状态管理:实现三级缓存系统(VRAM、RAM、Disk),支持会话状态持久化
  • 连续批处理:动态管理批次,提高 GPU 利用率
  • 多接口支持:提供聊天、翻译、代码补全等多种应用场景的专用接口

如果您需要深入了解底层实现细节、进行性能调优或对比不同优化方案,建议参考 Albatross 项目的源代码和基准测试脚本。Albatross 提供了更底层的实现细节,而 rwkv_lightning 则专注于提供易用的服务化接口。

3.4 启动推理服务

rwkv_lightning 以 Robyn 版本为主,提供密码认证、多接口、状态管理等特性,适合生产环境使用。Robyn 版本功能更全面,支持密码认证、多接口、状态管理等高级特性,适合生产环境使用。

python main_robyn.py --model-path /path/to/model --port 8000 --password rwkv7_7.2b

如果不需要密码保护,可以省略 --password 参数:

python main_robyn.py --model-path /path/to/model --port 8000

3.5 模型加载机制

了解模型加载机制有助于排查问题和优化性能。

权重加载流程

模型类 RWKV_x070 在初始化时会执行以下步骤:

  1. 读取权重文件:使用 torch.load(args.MODEL_NAME + '.pth', map_location='cpu') 将权重加载到 CPU 内存
  2. 数据类型转换:将权重转换为半精度(dtype=torch.half)以节省显存
  3. 设备迁移:根据硬件平台将权重移动到 GPU
    • NVIDIA GPU:使用 device="cuda"
    • AMD GPU:使用 ROCm 的 HIP 运行时

词表加载

词表文件 rwkv_batch/rwkv_vocab_v20230424.txt 通过 TRIE_TOKENIZER 类自动加载。TRIE 数据结构提供了高效的 token 查找和编码、解码功能。

CUDA、HIP 内核编译

项目包含自定义的 CUDA(NVIDIA)和 HIP(AMD)内核,用于加速 RWKV 的核心计算。这些内核在首次导入 rwkv_batch.rwkv7 模块时通过 torch.utils.cpp_extension.load 自动编译和加载:

  • CUDA 内核:rwkv_batch/cuda/rwkv7_state_fwd_fp16.cu
  • HIP 内核:rwkv_batch/hip/rwkv7_state_fwd_fp16.hip

首次运行时会进行编译,可能需要几分钟时间。编译后的内核会被缓存,后续启动会更快。

3.6 HTTP API 接口

rwkv_lightning 提供了丰富的 HTTP API 接口,支持多种推理场景。

聊天完成接口

  • v1/chat/completions:基础批量同步处理接口,支持流式和非流式输出。
  • v2/chat/completions:连续批处理接口,动态管理批次以提高 GPU 利用率,适合高并发场景。
  • v3/chat/completions:异步批处理接口,使用 CUDA Graph 优化(batch_size=1 时),提供最低延迟。

Fill-in-the-Middle 接口

FIM/v1/batch-FIM:支持代码和文本的中间填充补全,适用于代码补全、文本编辑等场景。

批量翻译接口

translate/v1/batch-translate:批量翻译接口,兼容沉浸式翻译插件的 API 格式,支持多语言互译。

会话状态管理接口

state/chat/completions:支持会话状态缓存的对话接口,实现多轮对话的上下文保持。状态采用三级缓存设计:

  • L1 缓存:VRAM(显存),最快访问
  • L2 缓存:RAM(内存),中等速度
  • L3 缓存:SQLite 数据库(磁盘),持久化存储

流式推理示例

以下示例展示如何使用 v2 接口进行批量流式推理:

curl -N -X POST http://localhost:8000/v2/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "contents": [
      "English: After a blissful two weeks, Jane encounters Rochester in the gardens.\n\nChinese:",
      "English: That night, a bolt of lightning splits the same chestnut tree.\n\nChinese:"
    ],
    "max_tokens": 1024,
    "stop_tokens": [0, 261, 24281],
    "temperature": 0.8,
    "top_k": 50,
    "top_p": 0.6,
    "alpha_presence": 1.0,
    "alpha_frequency": 0.1,
    "alpha_decay": 0.99,
    "stream": true,
    "chunk_size": 128,
    "password": "rwkv7_7.2b"
  }'

3.7 快速测试与性能评估

快速测试

项目提供了测试脚本,可以快速验证服务是否正常运行:

bash ./test_curl.sh

该脚本会发送示例请求到本地服务,检查各个接口的基本功能。

性能基准测试

使用 benchmark.py 脚本可以评估模型的推理性能,包括吞吐量、延迟等指标:

# 需要先修改 benchmark.py 中的模型路径
python benchmark.py

基准测试会输出详细的性能报告,帮助您了解模型在不同配置下的表现。

3.8 插件配置

在 VS Code 中打开设置(可搜索 rwkv-code-completion 或执行命令 RWKV: 打开设置),重点配置:

配置项 说明 示例
rwkv-code-completion.enabled 是否启用补全 true
rwkv-code-completion.baseUrl 后端基础地址,不含路径 http://192.168.0.157:8000http://localhost:8000
rwkv-code-completion.password --password 一致 rwkv7_7.2b
rwkv-code-completion.maxTokens 单次生成最大 token 数 200
rwkv-code-completion.numChoices 普通补全的候选数量(1–50) 24
rwkv-code-completion.debounceDelay 防抖延迟(毫秒) 150300

baseUrl 只需填 http(s)://host:port,插件内部会拼上 /v2/chat/completions/FIM/v1/batch-FIM。若设置界面中仅有 endpoint 等项,可在 settings.json 中手动添加 "rwkv-code-completion.baseUrl": "http://<主机>:<端口>"

3.9 验证接入

可先用 curl -X POST http://<host>:<port>/v2/chat/completions -H "Content-Type: application/json" -d '{"contents":["你好"],"max_tokens":10,"password":"<你的password>"}' 或运行 ./test_curl.sh 确认 v2 与 FIM 接口正常。在任意代码文件中输入、换行或删除,防抖后应出现「🤖 RWKV 正在生成 N 个代码补全...」并弹出侧边栏展示多个候选;若失败,可查看「输出」中该扩展的 channel 或弹窗报错,检查 baseUrlpassword、端口与防火墙。


四、常见问题

为何不能在 CPU 上运行?

rwkv_lightning 的核心计算依赖自定义的 CUDA、HIP 内核,这些内核专门为 GPU 并行计算设计。CPU 无法执行这些内核代码,因此必须使用 GPU。如果您需要在 CPU 上运行 RWKV 模型,请使用 llama.cpp,它提供了针对 CPU 优化的实现。

模型权重应该放在哪里?

模型权重可以放在任何可访问的路径。启动服务时通过 --model-path 参数指定路径即可。路径可以是绝对路径或相对路径,程序会自动处理 .pth 后缀的添加。

首次启动为什么很慢?

首次启动时会编译 CUDA、HIP 内核,这个过程可能需要几分钟。编译后的内核会被缓存,后续启动会快很多。如果希望进一步优化性能,可以考虑使用 torch.compile 模式(详见 README.md 中的 Tips 部分)。

如何选择合适的接口?

  • v1:适合简单的批量推理需求
  • v2:适合高并发场景,需要动态批处理
  • v3:适合单请求低延迟场景(batch_size=1)
  • FIM:适合代码补全和文本编辑
  • state:适合需要保持上下文的对话场景

本插件已按「无 suffix 用 v2、有 suffix 用 FIM」自动选择。

如何实现自动下载模型?

当前版本不提供内置的自动下载功能。您可以在启动脚本中添加下载逻辑,使用 huggingface_hub 库在启动前检查并下载模型文件。

主Agent与多个协同子Agent的方案设计

作者 sorryhc
2026年1月21日 17:51

前言

如今的大模型应用架构设计基本都是一个主Agent携带多个子Agent。

主Agent负责调度其他垂类Agent,子Agent负责单一领域的角色,属于垂直域专家。

架构上比较类似这样:

┌─────────────────────────────────────────────────────────┐
│                    主 Agent(Orchestrator)              │
│  职责:理解用户意图、分解任务、协调子 Agent、聚合结果   │
└──────────────────────┬──────────────────────────────────┘
                       │
        ┌──────────────┼──────────────┬──────────────┐
        │              │              │              │
        ▼              ▼              ▼              ▼
   ┌────────┐    ┌────────┐    ┌────────┐    ┌────────┐
   │差旅Agent│   │日程Agent│   │支付Agent│   │通知Agent│
   │(Travel)│   │(Calendar)│  │(Payment)│  │(Alert) │
   └────────┘    └────────┘    └────────┘    └────────┘
        │              │              │              │
        └──────────────┴──────────────┴──────────────┘
                       │
        ┌──────────────┼──────────────┐
        │              │              │
        ▼              ▼              ▼
   数据库           API 服务        外部服务
   (DB)          (Flights,        (Payment,
                  Hotels,          Email,
                 Trains)          SMS)

那一个基本的LLM应用框架一般怎么设计?本文基于Midwayjs来解读分析。

Agent&提示词设计

基类Agent

所有Agent都集成于该类,核心触发如下能力。

  1. 上下文管理;
  2. 大模型调用;
  3. 提示词注入;
// src/agent/base-agent.ts
import { Logger } from "@midwayjs/core"
import { LLMService } from "@/service/llm.service"

interface Message {
  role: "system" | "user" | "assistant"
  content: string
}

interface ToolCall {
  name: string
  arguments: Record<string, any>
  id?: string
}

/**
 * Agent 基类
 * 所有 Agent 都继承这个基类
 */
export abstract class BaseAgent {
  @Logger()
  logger: any

  protected llmService: LLMService
  protected conversationHistory: Message[] = []

  constructor(llmService: LLMService) {
    this.llmService = llmService
  }

  /**
   * 初始化 Agent
   * 1. 设置系统提示词
   * 2. 注入工具定义
   * 3. 初始化对话历史
   */
  protected initializeAgent(
    systemPrompt: string,
    tools: any[]
  ): void {
    this.logger.info(`[${this.getAgentName()}] 初始化 Agent`)

    // Step 1: 清空历史对话
    this.conversationHistory = []

    // Step 2: 添加系统提示词
    const enrichedSystemPrompt = this.enrichSystemPrompt(
      systemPrompt,
      tools
    )

    this.conversationHistory.push({
      role: "system",
      content: enrichedSystemPrompt,
    })

    this.logger.info(
      `[${this.getAgentName()}] Agent 初始化完成,已注入 ${tools.length} 个工具`
    )
  }

  /**
   * 增强系统提示词(注入工具定义)
   */
  private enrichSystemPrompt(systemPrompt: string, tools: any[]): string {
    const toolDescriptions = tools
      .map(
        (tool) => `
### 工具:${tool.name}
描述:${tool.description}
参数:${JSON.stringify(tool.parameters, null, 2)}
`
      )
      .join("\n")

    return `
${systemPrompt}

## 可用的工具

${toolDescriptions}

## 工具调用格式

当你需要使用工具时,请返回以下 JSON 格式:
\`\`\`json
{
  "type": "tool_call",
  "tool_name": "工具名称",
  "arguments": {
    "参数1": "值1",
    "参数2": "值2"
  }
}
\`\`\`

重要:
1. 每次只调用一个工具
2. 工具会返回结果,你会收到 "tool_result" 角色的消息
3. 根据工具结果继续推理和决策
4. 最终向用户返回友好的文字回复
`
  }

  /**
   * 与大模型交互(核心方法)
   */
  async callLLM(userMessage: string): Promise<string> {
    this.logger.info(
      `[${this.getAgentName()}] 用户消息: ${userMessage}`
    )

    // 1. 添加用户消息到历史
    this.conversationHistory.push({
      role: "user",
      content: userMessage,
    })

    // 2. 调用大模型
    let response = await this.llmService.call({
      model: "gpt-4",
      messages: this.conversationHistory,
      temperature: 0.7,
      maxTokens: 2000,
    })

    this.logger.info(
      `[${this.getAgentName()}] 模型响应: ${response.content.substring(0, 100)}...`
    )

    // 3. 检查是否是工具调用
    let finalResponse = response.content
    let toolCalls = this.extractToolCalls(response.content)

    // 4. 如果有工具调用,递归执行直到没有工具调用
    while (toolCalls.length > 0) {
      this.logger.info(
        `[${this.getAgentName()}] 检测到工具调用: ${toolCalls.map((t) => t.name).join(", ")}`
      )

      // 添加助手的响应到历史
      this.conversationHistory.push({
        role: "assistant",
        content: response.content,
      })

      // 执行所有工具调用
      const toolResults = await Promise.all(
        toolCalls.map((call) =>
          this.executeTool(call.name, call.arguments)
        )
      )

      // 5. 将工具结果添加到历史
      const toolResultMessage = toolResults
        .map(
          (result, index) => `
[工具结果 ${index + 1}]
工具:${toolCalls[index].name}
参数:${JSON.stringify(toolCalls[index].arguments)}
结果:${JSON.stringify(result, null, 2)}
`
        )
        .join("\n")

      this.conversationHistory.push({
        role: "user",
        content: `工具执行结果:\n${toolResultMessage}`,
      })

      this.logger.info(
        `[${this.getAgentName()}] 工具执行完成,继续推理...`
      )

      // 6. 再次调用大模型,让它基于工具结果继续推理
      response = await this.llmService.call({
        model: "gpt-4",
        messages: this.conversationHistory,
        temperature: 0.7,
        maxTokens: 2000,
      })

      this.logger.info(
        `[${this.getAgentName()}] 后续模型响应: ${response.content.substring(0, 100)}...`
      )

      // 7. 再次检查是否有工具调用
      toolCalls = this.extractToolCalls(response.content)
      finalResponse = response.content
    }

    // 8. 添加最终回复到历史
    this.conversationHistory.push({
      role: "assistant",
      content: finalResponse,
    })

    return finalResponse
  }

  /**
   * 提取工具调用(从模型响应中)
   */
  private extractToolCalls(content: string): ToolCall[] {
    const toolCalls: ToolCall[] = []

    // 匹配 JSON 格式的工具调用
    const jsonMatches = content.match(/```json\n([\s\S]*?)\n```/g)

    if (jsonMatches) {
      jsonMatches.forEach((match) => {
        try {
          const json = match.replace(/```json\n/g, "").replace(/\n```/g, "")
          const parsed = JSON.parse(json)

          if (parsed.type === "tool_call") {
            toolCalls.push({
              name: parsed.tool_name,
              arguments: parsed.arguments,
            })
          }
        } catch (error) {
          this.logger.warn(`[${this.getAgentName()}] 无法解析 JSON: ${match}`)
        }
      })
    }

    return toolCalls
  }

  /**
   * 执行工具(由子类实现)
   */
  protected abstract executeTool(
    toolName: string,
    arguments: Record<string, any>
  ): Promise<any>

  /**
   * 获取 Agent 名称
   */
  protected abstract getAgentName(): string
}

工具定义&设计

工具定义核心是基于约定式的配置体,来提供给大模型。

这些工具可以是mcp,可以是function call,在工具中增加type即可扩展。

// src/tools/travel-tools.ts

/**
 * 差旅工具定义
 * 这些工具会被注入到 Agent 的提示词中
 */
export const TRAVEL_TOOLS = [
  {
    name: "search_flights",
    description: "搜索机票,返回可用的航班列表",
    parameters: {
      type: "object",
      properties: {
        from: {
          type: "string",
          description: "出发城市(如:北京、上海)",
        },
        to: {
          type: "string",
          description: "目的城市",
        },
        date: {
          type: "string",
          description: "出发日期(格式:YYYY-MM-DD)",
        },
        return_date: {
          type: "string",
          description: "返回日期(可选,格式:YYYY-MM-DD)",
        },
      },
      required: ["from", "to", "date"],
    },
  },
  {
    name: "search_hotels",
    description: "搜索酒店,返回可用的酒店列表",
    parameters: {
      type: "object",
      properties: {
        city: {
          type: "string",
          description: "目的城市",
        },
        check_in: {
          type: "string",
          description: "入住日期(格式:YYYY-MM-DD)",
        },
        check_out: {
          type: "string",
          description: "退房日期(格式:YYYY-MM-DD)",
        },
        max_price: {
          type: "number",
          description: "最高价格(可选,单位:元)",
        },
      },
      required: ["city", "check_in", "check_out"],
    },
  },
  {
    name: "book_trip",
    description: "预订机票和酒店,返回订单号",
    parameters: {
      type: "object",
      properties: {
        flight_id: {
          type: "string",
          description: "航班 ID",
        },
        hotel_id: {
          type: "string",
          description: "酒店 ID",
        },
        passengers: {
          type: "number",
          description: "乘客人数",
        },
      },
      required: ["flight_id", "hotel_id"],
    },
  },
  {
    name: "get_trip_details",
    description: "获取已预订差旅的详细信息",
    parameters: {
      type: "object",
      properties: {
        trip_id: {
          type: "string",
          description: "订单号",
        },
      },
      required: ["trip_id"],
    },
  },
  {
    name: "cancel_trip",
    description: "取消已预订的差旅",
    parameters: {
      type: "object",
      properties: {
        trip_id: {
          type: "string",
          description: "订单号",
        },
        reason: {
          type: "string",
          description: "取消原因(可选)",
        },
      },
      required: ["trip_id"],
    },
  },
]

export const CALENDAR_TOOLS = [
  {
    name: "add_calendar_event",
    description: "添加日历事件",
    parameters: {
      type: "object",
      properties: {
        title: {
          type: "string",
          description: "事件标题",
        },
        start_date: {
          type: "string",
          description: "开始时间(格式:YYYY-MM-DD HH:mm)",
        },
        end_date: {
          type: "string",
          description: "结束时间(格式:YYYY-MM-DD HH:mm)",
        },
        description: {
          type: "string",
          description: "事件描述",
        },
      },
      required: ["title", "start_date", "end_date"],
    },
  },
  {
    name: "get_calendar_events",
    description: "查询特定日期的日历事件",
    parameters: {
      type: "object",
      properties: {
        date: {
          type: "string",
          description: "查询日期(格式:YYYY-MM-DD)",
        },
      },
      required: ["date"],
    },
  },
]

export const PAYMENT_TOOLS = [
  {
    name: "process_payment",
    description: "处理支付请求",
    parameters: {
      type: "object",
      properties: {
        order_id: {
          type: "string",
          description: "订单号",
        },
        amount: {
          type: "number",
          description: "金额(单位:元)",
        },
        payment_method: {
          type: "string",
          enum: ["credit_card", "debit_card", "wechat", "alipay"],
          description: "支付方式",
        },
      },
      required: ["order_id", "amount", "payment_method"],
    },
  },
]

export const ALERT_TOOLS = [
  {
    name: "send_notification",
    description: "发送通知给用户",
    parameters: {
      type: "object",
      properties: {
        title: {
          type: "string",
          description: "通知标题",
        },
        content: {
          type: "string",
          description: "通知内容",
        },
        channels: {
          type: "array",
          items: { type: "string", enum: ["email", "sms", "app"] },
          description: "通知渠道",
        },
      },
      required: ["title", "content", "channels"],
    },
  },
]

MCP设计

Agent基于多个Mcp能力的提供从而实现更垂直的领域能力。

因此Mcp也可以单独设计出来。

// src/mcp/types.ts

/**
 * MCP 工具定义
 */
export interface MCPTool {
  name: string
  description: string
  inputSchema: {
    type: "object"
    properties: Record<string, any>
    required: string[]
  }
}

/**
 * MCP 资源定义
 */
export interface MCPResource {
  uri: string
  name: string
  description: string
  mimeType: string
  contents: string
}

/**
 * MCP 提示词定义
 */
export interface MCPPrompt {
  name: string
  description: string
  arguments?: Array<{
    name: string
    description: string
    required?: boolean
  }>
}

/**
 * MCP 工具调用请求
 */
export interface MCPToolCallRequest {
  toolName: string
  arguments: Record<string, any>
}

/**
 * MCP 工具执行结果
 */
export interface MCPToolResult {
  success: boolean
  data?: any
  error?: string
}

/**
 * MCP 服务器接口
 */
export interface IMCPServer {
  // 获取服务器信息
  getServerInfo(): Promise<{
    name: string
    version: string
    capabilities: string[]
  }>

  // 列出所有可用工具
  listTools(): Promise<MCPTool[]>

  // 执行工具
  callTool(request: MCPToolCallRequest): Promise<MCPToolResult>

  // 列出所有可用资源
  listResources(): Promise<MCPResource[]>

  // 获取资源内容
  getResource(uri: string): Promise<MCPResource>

  // 列出所有可用提示词
  listPrompts(): Promise<MCPPrompt[]>

  // 获取提示词内容
  getPrompt(name: string, arguments?: Record<string, string>): Promise<string>
}

有了AgentMcp,本质上完整的一次自然语言对话 -> 反馈的系统流转图就很清晰了。

基于这套框架来扩展即可。

一次完整对话到反馈的时序图大概是这样:

用户                主Agent              子Agent           MCP服务器         LLM模型          数据库
 │                   │                   │                 │                │                │
 │ 用户请求:         │                   │                 │                │                │
 │ "帮我订一张      │                   │                 │                │                │
 │  明天北京到      │                   │                 │                │                │
 │  上海的机票      │                   │                 │                │                │
 │  和酒店"        │                   │                 │                │                │
 │──────────────────>│                   │                 │                │                │
 │                   │                   │                 │                │                │
 │                   │ 1. 初始化对话      │                 │                │                │
 │                   │    构建系统提示词  │                 │                │                │
 │                   │────────────────────────────────────>│                │                │
 │                   │                   │                 │                │                │
 │                   │ 2. 请求可用工具列表│                 │                │                │
 │                   │──────────────────────────────────────────────────────>│                │
 │                   │                   │                 │                │                │
 │                   │ 3. 返回工具列表    │                 │                │                │
 │                   │<──────────────────────────────────────────────────────│                │
 │                   │    (search_flights, search_hotels,   │                │                │
 │                   │     book_trip, etc.)                 │                │                │
 │                   │                   │                 │                │                │
 │                   │ 4. 获取提示词模板  │                 │                │                │
 │                   │──────────────────────────────────────>│                │                │
 │                   │                   │                 │                │                │
 │                   │ 5. 返回提示词      │                 │                │                │
 │                   │<──────────────────────────────────────│                │                │
 │                   │   (booking_recommendation等)         │                │                │
 │                   │                   │                 │                │                │
 │                   │ 6. 构建系统消息    │                 │                │                │
 │                   │    (系统提示词+工具定义+提示词)      │                │                │
 │                   │    users消息="用户请求内容"         │                │                │
 │                   │                   │                 │                │                │
 │                   │ 7. 调用 LLM        │                 │                │                │
 │                   │──────────────────────────────────────────────────────>│                │
 │                   │                   │                 │                │  分析意图       │
 │                   │                   │                 │                │  (BOOK_TRIP)    │
 │                   │                   │                 │                │  提取参数       │
 │                   │                   │                 │                │  (from, to,date)│
 │                   │                   │                 │                │  生成工具调用   │
 │                   │                   │                 │                │                │
 │                   │ 8. LLM 响应        │                 │                │                │
 │                   │<──────────────────────────────────────────────────────│                │
 │                   │    {                                 │                │                │
 │                   │      "type": "tool_call",            │                │                │
 │                   │      "tool_name": "search_flights",  │                │                │
 │                   │      "arguments": {                  │                │                │
 │                   │        "from": "北京",                │                │                │
 │                   │        "to": "上海",                  │                │                │
 │                   │        "date": "明天"                 │                │                │
 │                   │      }                               │                │                │
 │                   │    }                                 │                │                │
 │                   │                   │                 │                │                │
 │                   │ 9. 检测到工具调用, │                 │                │                │
 │                   │    路由到子Agent   │                 │                │                │
 │                   │────────────────────>│                │                │                │
 │                   │                   │                 │                │                │
 │                   │                   │ 10. 子Agent     │                │                │
 │                   │                   │     处理工具调用 │                │                │
 │                   │                   │──────────────────>│                │                │
 │                   │                   │                 │                │                │
 │                   │                   │ 11. Travel MCP  │                │                │
 │                   │                   │     执行         │                │                │
 │                   │                   │     search_flights│               │                │
 │                   │                   │                 │ 查询数据库     │                │
 │                   │                   │                 │────────────────────────────────>│
 │                   │                   │                 │                │                │
 │                   │                   │                 │ 返回机票列表   │                │
 │                   │                   │                 │<────────────────────────────────│
 │                   │                   │                 │                │                │
 │                   │ 12. 返回工具结果   │                 │                │                │
 │                   │<────────────────────                 │                │                │
 │                   │    [                                │                │                │ │                   │      {                               │                │                │ │                   │        "id": "CA123",                │                │                │ │                   │        "airline": "国航",             │                │                │ │                   │        "departure": "10:00",         │                │                │ │                   │        "price": 1200                 │                │                │ │                   │      },                              │                │                │ │                   │      ...                             │                │                │ │                   │    ]                                 │                │                │
 │                   │                   │                 │                │                │
 │                   │ 13. 添加工具结果   │                 │                │                │
 │                   │     到对话历史     │                 │                │                │
 │                   │     再次调用 LLM   │                 │                │                │
 │                   │──────────────────────────────────────────────────────>│                │
 │                   │                   │                 │                │  分析机票      │
 │                   │                   │                 │                │  生成下一个工具│
 │                   │                   │                 │                │  调用:         │
 │                   │                   │                 │                │  search_hotels │
 │                   │                   │                 │                │                │
 │                   │ 14. LLM 响应(第2次)│                 │                │                │
 │                   │<──────────────────────────────────────────────────────│                │
 │                   │    {                                 │                │                │
 │                   │      "type": "tool_call",            │                │                │
 │                   │      "tool_name": "search_hotels",   │                │                │
 │                   │      "arguments": {                  │                │                │
 │                   │        "city": "上海",                │                │                │
 │                   │        "check_in": "明天",            │                │                │
 │                   │        "check_out": "后天"            │                │                │
 │                   │      }                               │                │                │
 │                   │    }                                 │                │                │
 │                   │                   │                 │                │                │
 │                   │ 15. 再次路由到子Agent│                │                │                │
 │                   │────────────────────>│                │                │                │
 │                   │                   │                 │                │                │
 │                   │                   │ 16. 执行        │                │                │
 │                   │                   │     search_hotels│               │                │
 │                   │                   │──────────────────>│                │                │
 │                   │                   │                 │                │ 查询酒店       │
 │                   │                   │                 │────────────────────────────────>│
 │                   │                   │                 │                │                │
 │                   │                   │                 │ 返回酒店列表   │                │
 │                   │                   │                 │<────────────────────────────────│
 │                   │                   │                 │                │                │
 │                   │ 17. 返回酒店结果   │                 │                │                │
 │                   │<────────────────────                 │                │                │
 │                   │                   │                 │                │                │
 │                   │ 18. 再次调用 LLM   │                 │                │                │
 │                   │     (决定下一步)   │                 │                │                │
 │                   │──────────────────────────────────────────────────────>│                │
 │                   │                   │                 │                │  分析酒店      │
 │                   │                   │                 │                │  推荐最佳套餐  │
 │                   │                   │                 │                │  生成工具调用: │
 │                   │                   │                 │                │  book_trip     │
 │                   │                   │                 │                │                │
 │                   │ 19. LLM 响应(第3次)│                 │                │                │
 │                   │<──────────────────────────────────────────────────────│                │
 │                   │    {                                 │                │                │
 │                   │      "type": "tool_call",            │                │                │
 │                   │      "tool_name": "book_trip",       │                │                │
 │                   │      "arguments": {                  │                │                │
 │                   │        "flight_id": "CA123",         │                │                │
 │                   │        "hotel_id": "SH001"           │                │                │
 │                   │      }                               │                │                │
 │                   │    }                                 │                │                │
 │                   │                   │                 │                │                │
 │                   │ 20. 路由到子Agent  │                 │                │                │
 │                   │ (预订差旅)         │                 │                │                │
 │                   │────────────────────>│                │                │                │
 │                   │                   │                 │                │                │
 │                   │                   │ 21. 执行book_trip│               │                │
 │                   │                   │──────────────────>│                │                │
 │                   │                   │                 │                │ 创建订单       │
 │                   │                   │                 │────────────────────────────────>│
 │                   │                   │                 │                │                │
 │                   │                   │                 │ 返回订单号     │                │
 │                   │                   │                 │<────────────────────────────────│
 │                   │                   │                 │                │                │
 │                   │ 22. 返回预订结果   │                 │                │                │
 │                   │<────────────────────                 │                │                │
 │                   │    {                                 │                │                │
 │                   │      "trip_id": "TRIP_001",          │                │                │
 │                   │      "status": "confirmed",          │                │                │
 │                   │      "total_cost": 3000              │                │                │
 │                   │    }                                 │                │                │
 │                   │                   │                 │                │                │
 │                   │ 23. 调用Calendar MCP│                │                │                │
 │                   │     添加日程        │                │                │                │
 │                   │────────────────────────────────────────────────────────>│               │
 │                   │                   │                 │                │ 添加日历事件   │
 │                   │                   │                 │────────────────────────────────>│
 │                   │                   │                 │                │                │
 │                   │                   │                 │ 返回事件ID     │                │
 │                   │                   │                 │<────────────────────────────────│
 │                   │                   │                 │                │                │
 │                   │ 24. 调用Payment MCP│                 │                │                │
 │                   │     处理支付        │                 │                │                │
 │                   │────────────────────────────────────────────────────────>│               │
 │                   │                   │                 │                │ 创建支付单     │
 │                   │                   │                 │────────────────────────────────>│
 │                   │                   │                 │                │                │
 │                   │                   │                 │ 返回交易ID     │                │
 │                   │                   │                 │<────────────────────────────────│
 │                   │                   │                 │                │                │
 │                   │ 25. 调用Alert MCP  │                 │                │                │
 │                   │     发送通知        │                 │                │                │
 │                   │────────────────────────────────────────────────────────>│               │
 │                   │                   │                 │                │ 记录通知        │
 │                   │                   │                 │────────────────────────────────>│
 │                   │                   │                 │                │                │
 │                   │ 26. 最后调用 LLM   │                 │                │                │
 │                   │     生成友好回复   │                 │                │                │
 │                   │──────────────────────────────────────────────────────>│                │
 │                   │                   │                 │                │ 总结整个过程    │
 │                   │                   │                 │                │ 生成用户友好    │
 │                   │                   │                 │                │ 的文字回复      │
 │                   │                   │                 │                │                │
 │                   │ 27. LLM 最终响应   │                 │                │                │
 │                   │<──────────────────────────────────────────────────────│                │
 │                   │    "好的,已为您预订了从北京       │                │                │
 │                   │     到上海的差旅。您的订单号是    │                │                │
 │                   │     TRIP_001,总费用3000元。     │                │                │
 │                   │     已添加到您的日程,并发送

本质上一句话总结:对话发起后,主Agent构建基础提示词进行首轮行为分析后,然后按需注入子Agent来递归/循环完成一轮对话。

结尾

如上就非常简单直观的结合代码,讲解了现在LLM大模型应用的核心架构和角色拆解。

希望对大家有所帮助。

❌
❌