今天是 202 年 4 月 16 日
大模型基础入门
认识AI和大模型
我们熟知的大模型,例如GPT、DeepSeek 底层都是采用 Transformer 神经网络模型。
- G(Generative):生成式,根据上下文预测之后应该出现哪个文本,从而形成连续的文本输出。
- P(Pre-Trained):预训练,通过大规模的文本数据进行预训练,让大模型可以理解人类的语法、词性。
- T(Transformer):深度学习的一种神经网络模型。多数 AIGC 模型都依赖于此。

Prompt Engineering
什么是提示词工程(Prompt Engineering)?
提示工程也叫「指令工程」。
- Prompt 就是你发给大模型的指令,比如「讲个笑话」、「用 Python 编个贪吃蛇游戏」、「给男/女朋友写封情书」等
- 貌似简单,但意义非凡
- 「Prompt」是 AGI 时代的「编程语言」
- 「Prompt 工程」是 AGI 时代的「软件工程」
- 「提示工程师」是 AGI 时代的「程序员」
- 学会提示工程,就像学用鼠标、键盘一样,是 AGI 时代的基本技能
- 提示工程也是**「门槛低,天花板极高」**,所以有人戏称 Prompt 为「咒语」
- 但专门的「提示工程师」不会长久,因为每个人都要会「提示词工程」,AI 的进化也会让提示词工程越来越简单
使用Prompt的两种目的
- 获得具体问题的具体结果,比如「我该学 Vue 还是 React?」「PHP 为什么是最好的语言?」
- 固化一套 Prompt 到程序中,成为系统功能的一部分,比如「每天生成公司的简报」「AI 客户系统」「基于公司知识库的问答」
前者主要通过 ChatGPT、ChatALL 这样的操作;后者就要动代码了。我们专注于后者,因为:
- 后者更难,掌握后能轻松搞定前者
- 后者是我们的独特优势
高质量 Prompt 核心要点:具体、丰富、少歧义
AI应用框架
Spring AI Alibaba
训练营的项目也是基于Spring AI Alibaba(SSA)框架的。
Spring AI Alibaba、Spring AI、LangChain4J 三个必须要有其中一个,推荐就 Spring AI Alibaba 吧。
第一个案例手敲,加深印象。

- API Key:sk-xxx 你自己的 API Key
- 模型名:qwen-plus
- 调用地址:使用 SDK 调用时需配置的 base_url
xml
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<!--Spring Boot-->
<spring-boot.version>3.5.5</spring-boot.version>
<!--Spring AI-->
<spring-ai.version>1.0.0</spring-ai.version>
<!--Spring AI Alibaba-->
<SpringAIAlibaba.version>1.0.0.2</SpringAIAlibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring AI Alibaba -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>${SpringAIAlibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring AI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>

添加到环境变量:

properties
server.port=8001
# 大模型对话中文乱码,UTF8编码处理
server.servlet.encoding.enabled=true
server.servlet.encoding.force=true
server.servlet.encoding.charset=UTF-8
spring.application.name=SAA-01HelloWorld
spring.ai.dashscope.api-key=${DASHSCOPE_API_KEY}
spring.ai.dashscope.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1
spring.ai.dashscope.chat.options.model=qwen-plus
java
package com.hwl.study.controller;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
public class CharHelloController {
// 对话模型,调用阿里云百炼平台
@Resource
private ChatModel chatModel;
/**
* 通用调用
* @param msg
* @return
*/
@GetMapping(value = "/hello/dochat")
public String doChat(@RequestParam(name = "msg", defaultValue = "你是谁") String msg) {
return chatModel.call(msg);
}
/**
* 流式返回调用
* @param msg
* @return
*/
@GetMapping(value = "/hello/streamchat")
public Flux<String> stream(@RequestParam(name = "msg", defaultValue = "你是谁") String msg) {
return chatModel.stream(msg);
}
}

下面这种是不阻塞的,出一点拿一点,挤牙膏一样。




- ChatModel 是底层的接口,直接与大语言模型进行交互,提供 call 和 stream 方法,适合简单大模型交互场景。
- ChatClient 是高级封装,基于 ChatModel 构建,适合快速构建标准化复杂 AI 服务,支持同步和流式交互,集成多种高级功能。
原本:
java
@RestController
public class ChatClientController {
@Resource
private ChatModel dashScopeModel;
@GetMapping("/chatmodel/dochat")
public String doChat(@RequestParam(name = "msg", defaultValue = "你是谁") String msg) {
String result = dashScopeModel.call(msg);
System.out.println("响应是:" + result);
return result;
}
}


可见,ChatClient 是不支持自动注入的,只能手动注入
java
@RestController
public class ChatClientController {
private final ChatClient dashScopeChatClient;
/**
* chatClient 不支持自动注入,依赖于 ChatModel 对象接口
* ChatClient.builder(dashScopeChatModel).build();
*/
public ChatClientController(ChatModel dashScopeChatModel) {
this.dashScopeChatClient = ChatClient.builder(dashScopeChatModel).build();
}
@GetMapping("/chatclient/dochat")
public String doChat(@RequestParam(name = "msg", defaultValue = "2乘2等于多少") String msg) {
return dashScopeChatClient.prompt().user(msg).call().content();
}
}

ChatModel 对 ChatClient 的吐槽:离开了我,你啥也不是。😎
若要自动注入,要自己去写个配置类:
java
package com.hwl.study.config;
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SaaLLMConfig {
@Bean
public DashScopeApi dashScopeApi() {
return DashScopeApi.builder().apiKey(System.getenv("DASHSCOPE_API_KEY")).build();
}
@Bean
public ChatClient chatClient(ChatModel dashScopeChatModel) {
return ChatClient.builder(dashScopeChatModel).build();
}
}
java
@RestController
public class ChatClientControllerV2 {
@Resource
private ChatModel chatModel;
@Resource
private ChatClient dashScopeChatClientV2;
@GetMapping("/chatclientv2/dochat")
public String doChat(@RequestParam(name = "msg", defaultValue = "你是谁") String msg) {
String result = dashScopeChatClientV2.prompt().user(msg).call().content();
System.out.println("ChatClient的响应: " + result);
return result;
}
@GetMapping("/chatmodelv2/dochat")
public String doChat2(@RequestParam(name = "msg", defaultValue = "你是谁") String msg) {
String result = chatModel.call(msg);
System.out.println("ChatModel的响应:" + result);
return result;
}
}

实际使用中,ChatClient 和 ChatModel 是混合使用的。都要掌握。

SSE(Server-Sent-Events)是一种允许服务端可以持续推送数据片段(如逐词或逐句)到前端 的 Web 技术。通过单向的 HTTP 长连接,使用一个长期存在的连接,让服务器可以主动将数据"推"给客户端,SSE 是轻量级的单向通信协议,适合 AI 对话这类服务端主导的场景。
SSE 的核心思想是:客户端发起一个请求,服务端保持这个连接打开并在新数据时,通过这个连接将数据发送给客户端。这与传统的请求-响应模式(客户端请求一次,服务器响应一次,连接关闭)有本质区别。SSE 的下一代(Streamable HTTP)。

通过 ChatModel 实现 stream 实现流式输出:
java
@Configuration
public class SaaLLMConfig {
private final String DEEPSEEK_MODEL = "deepseek-v3";
private final String QWEN_MODEL = "qwen-plus";
@Bean(name = "deepseek")
public ChatModel deepSeek() {
return DashScopeChatModel.builder()
.dashScopeApi(DashScopeApi.builder().apiKey(System.getenv("DASHSCOPE_API_KEY")).build())
.defaultOptions(DashScopeChatOptions.builder().withModel(DEEPSEEK_MODEL).build())
.build();
}
@Bean(name = "qwen")
public ChatModel qwen() {
return DashScopeChatModel.builder()
.dashScopeApi(DashScopeApi.builder().apiKey(System.getenv("DASHSCOPE_API_KEY")).build())
.defaultOptions(DashScopeChatOptions.builder().withModel(QWEN_MODEL).build())
.build();
}
}
java
@RestController
public class StreamOutputController {
@Resource(name = "deepseek")
private ChatModel deepseekChatModel;
@Resource(name = "qwen")
private ChatModel qwenChatModel;
@GetMapping(value = "/stream/chatflux1")
public Flux<String> chatflux(@RequestParam(name = "question", defaultValue = "你是谁") String question) {
return deepseekChatModel.stream(question);
}
@GetMapping(value = "/stream/chatflux2")
public Flux<String> chatflux2(@RequestParam(name = "question", defaultValue = "你是谁") String question) {
return qwenChatModel.stream(question);
}
}
现在用 ChatClient 实现 stream 流式输出:
首先也是要加上配置:
java
package com.hwl.study.config;
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SaaLLMConfig {
private final String DEEPSEEK_MODEL = "deepseek-v3";
private final String QWEN_MODEL = "qwen-plus";
@Bean(name = "deepseek")
public ChatModel deepSeek() {
return DashScopeChatModel.builder()
.dashScopeApi(DashScopeApi.builder().apiKey(System.getenv("DASHSCOPE_API_KEY")).build())
.defaultOptions(DashScopeChatOptions.builder().withModel(DEEPSEEK_MODEL).build())
.build();
}
@Bean(name = "qwen")
public ChatModel qwen() {
return DashScopeChatModel.builder()
.dashScopeApi(DashScopeApi.builder().apiKey(System.getenv("DASHSCOPE_API_KEY")).build())
.defaultOptions(DashScopeChatOptions.builder().withModel(QWEN_MODEL).build())
.build();
}
@Bean(name = "deepseekChatClient")
public ChatClient deepseekChatClient(@Qualifier("deepseek") ChatModel deepseek) {
return
ChatClient.builder(deepseek)
.defaultOptions(ChatOptions.builder().model(DEEPSEEK_MODEL).build())
.build();
}
@Bean(name = "qwenChatClient")
public ChatClient qwenChatClient(@Qualifier("qwen") ChatModel qwen) {
return ChatClient.builder(qwen)
.defaultOptions(ChatOptions.builder().model(QWEN_MODEL).build())
.build();
}
}

html
<body>
<textarea id="messageInput" rows="4" cols="50" placeholder="请输入你的问题..."></textarea><br>
<button onclick="sendMsg()">发送提问</button>
<div id="messages"></div>
<script>
function sendMsg() {
// 获取用户输入的消息
const message = document.getElementById('messageInput').value;
if (message === "") return false;
//1 客户端使用 JavaScript 的 EventSource 对象连接到服务器上的一个特定端点(URL)
const eventSource = new EventSource('stream/chatflux2?question=' + message);
//2 监听消息事件
eventSource.onmessage = function (event) {
// 获取流式返回的数据
const data = event.data;
// 将接收到的数据展示到页面上
const messagesDiv = document.getElementById('messages');
messagesDiv.innerHTML += data;
};
//3 监听错误事件
eventSource.onerror = function (error) {
console.error('EventSource 发生错误:', error);
eventSource.close(); // 关闭连接
};
}
</script>
</body>
提示词Prompt



Prompt:通常使用 ChatModel 的 call() 方法,该方法接收 Prompt 实例并返回 ChatResponse。

可以近似地理解:
Prompt > Message > String 简单的字符串
Prompt 中的四大角色:



- SYSTEM:设定AI的行为边界/角色/定位,指导AI的行为和响应方式,设置AI如何解释和回复输入的
- USER:用户原始提问输入,代表用户的输入,他们向AI提出的问题、命令或陈述。
- ASSISTANT:AI返回的响应信息,定义为"助手角色"消息,用它可以确保上下文能够连贯的交互。(记忆对话,积累回答)
- TOOL:桥接外部服务,可以进行函数调用。如:支付/数据查询等操作,类似调用第三方 util 工具类。(后面有详细章节介绍)

java
// http://localhost:8005/prompt/chat?question=火锅介绍下
@GetMapping("/prompt/chat")
public Flux<String> chat(String question) {
return deepseekChatClient.prompt()
//system: AI能力边界
.system("你是一个法律助手,只回答法律问题,其它问题回复,我只能回答法律相关问题,其它无可奉告")
.user(question)
.stream()
.content();
}

使用 ChatModel:
java
// http://localhost:8005/prompt/chat2?question=葫芦娃
@GetMapping("/prompt/chat2")
public Flux<ChatResponse> chat2(String question) {
SystemMessage systemMessage = new SystemMessage("你是一个讲故事的助手,每个故事控制在300字以内");
// 用户消息
UserMessage userMessage = new UserMessage(question);
Prompt prompt = new Prompt(userMessage, systemMessage);
return deepseekChatModel.stream(prompt);
}

java
// http://localhost:8005/prompt/chat3?question=葫芦娃
@GetMapping("/prompt/chat3")
public Flux<String> chat3(String question) {
// 系统消息
SystemMessage systemMessage = new SystemMessage("你是一个讲故事的助手," +
"每个故事控制在600字以内且以HTML格式返回");
// 用户消息
UserMessage userMessage = new UserMessage(question);
Prompt prompt = new Prompt(userMessage, systemMessage);
return deepseekChatModel.stream(prompt)
.map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText());
}

java
// http://localhost:8005/prompt/chat4?question=葫芦娃
@GetMapping("/prompt/chat4")
public String chat4(String question){
AssistantMessage assistantMessage = deepseekChatClient.prompt()
.user(question)
.call()
.chatResponse()
.getResult()
.getOutput();
return assistantMessage.getText();
}

java
// http://localhost:8005/prompt/chat5?city=重庆
@GetMapping("/prompt/chat5")
public String chat5(String city) {
String answer = qwenChatClient.prompt()
.user(city + "未来3天天气情况如何?")
.call()
.chatResponse()
.getResult()
.getOutput()
.getText();
ToolResponseMessage toolResponseMessage = new ToolResponseMessage(
List.of(new ToolResponseMessage.ToolResponse("1", "获得天气", city))
);
String toolResponse = toolResponseMessage.getText();
return answer + toolResponse;
}

提示词模板
所谓的提示词模板,就是要引入占位符(如 {占位符变量名})以动态地插入内容
java
/**
* 演示 PromptTemplate 的基本使用,使用占位符设置模板 PromptTemplate
* http://localhost:8006/prompttemplate/chat?topic=java&output_format=html&wordCount=200
*/
@GetMapping("/prompttemplate/chat")
public Flux<String> chat(String topic, String output_format, String wordCount) {
PromptTemplate promptTemplate = new PromptTemplate("" +
"讲一个关于{topic}的故事" +
"并以{output_format}格式输出," +
"字数在{wordCount}左右");
//promptTemplate -> Prompt
Prompt prompt = promptTemplate.create(Map.of(
"topic", topic,
"output_format", output_format,
"wordCount", wordCount
));
return deepseekChatClient.prompt(prompt).stream().content();
}

上面这种,是在代码里面写死了。不合适。所以下面采用 PromptTemplate 读取模板文件来实现模板功能。

多角色设定
java
/**
* 系统消息(SystemMessage):设定AI的行为规则和功能边界(xxx助手/什么格式返回/字数控制多少)
* 用户消息(UserMessage):用户的提问/主题
* http:localhost:8006/prompttemplate/chat3?sysTopic=法律&userTopic=知识产权法
* http:localhost:8006/prompttemplate/chat3?sysTopic=法律&userTopic=夫妻肺片
*/
@GetMapping("/prompttemplate/chat3")
public String chat3(String sysTopic, String userTopic) {
// 1.SystemPromptTemplate
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate("你是{systemTopic}助手,只回答{systemTopic}。" +
"其它无可奉告,并以HTML格式返回");
Message sysMessage = systemPromptTemplate.createMessage(Map.of("systemTopic", sysTopic));
// 2PromptTemplate
PromptTemplate userPromptTemplate = new PromptTemplate("解释一下{userTopic}");
Message userMessage = userPromptTemplate.createMessage(Map.of("userTopic", userTopic));
// 3.组合【关键】多个 Message -> Prompt
Prompt prompt = new Prompt(List.of(sysMessage, userMessage));
// 4.调用LLM
return deepseekChatClient.prompt(prompt).call().content();
}


人物设定
java
/**
* 人物角色设定,通过SystemMessage来实现人物设定,本案例用 ChatModel实现
* 设定AI为"医疗专家"时,仅回答医学相关问题
* 设定AI为"编程助手"时,专注于技术问题解答
* http://localhost:8006/prompttemplate/chat4?question=刘备
*/
@GetMapping("/prompttemplate/chat4")
public String chat4(String question) {
// 1.系统消息
SystemMessage systemMessage = new SystemMessage("你是一个Java编程助手,拒绝回答非技术问题");
// 2.用户消息
UserMessage userMessage = new UserMessage(question);
// 3.系统消息+用户消息=完整提示词
// Prompt prompt = new Prompt(systemMessage, userMessage);
Prompt prompt = new Prompt(List.of(systemMessage, userMessage));
// 4.调用 LLM
String result = deepseekChatModel.call(prompt).getResult().getOutput().getText();
System.out.println(result);
return result;
}

java
/**
* 人物角色设定,通过SystemMessage来实现人物设定,本案例用 ChatModel实现
* 设定AI为"医疗专家"时,仅回答医学相关问题
* 设定AI为"编程助手"时,专注于技术问题解答
* http://localhost:8006/prompttemplate/chat5?question=火锅
*/
@GetMapping("/prompttemplate/chat5")
public Flux<String> chat5(String question){
return deepseekChatClient.prompt()
.system("你是一个Java编程助手,拒绝回答非技术问题。")
.user(question)
.stream()
.content();
}

格式化输出

我们希望将输出转换为 Record 记录类结构体,不再是传统的 String。
JDK 14的新特性:
22 分钟后开始观看。
java
/**
* JDK 14以后的新特性,记录类record = entity + lombok
*/
public record StudentRecord(String id, String sname, String major, String email) { }
java
@RestController
public class StructuredOutputController {
@Resource(name = "qwenChatClient")
private ChatClient qwenChatClient;
// http://localhost:8007/structuredoutput/chat?sname=李四&email=zzyybs@126.com
@GetMapping("/structuredoutput/chat")
public StudentRecord chat(@RequestParam(name = "sname") String sname, @RequestParam(name = "email") String email) {
return qwenChatClient.prompt().user(new Consumer<ChatClient.PromptUserSpec>() {
@Override
public void accept(ChatClient.PromptUserSpec promptUserSpec) {
promptUserSpec.text("学号1001,我叫{sname},大学专业是计算机科学与技术,邮箱{email}")
.param("sname", sname)
.param("email", email);
}
}).call().entity(StudentRecord.class);
}
// http://localhost:8007/structuredoutput/chat2?sname=孙伟&email=zzyybs@126.com
@GetMapping("/structuredoutput/chat2")
public StudentRecord chat2(@RequestParam(name = "sname") String sname, @RequestParam(name = "email") String email) {
String stringTemplate = """
学号1002,我叫{sname},大学专业软件工程,邮箱{email}
""";
return qwenChatClient.prompt()
.user(promptUserSpec -> promptUserSpec.text(stringTemplate)
.param("sname", sname)
.param("email", email))
.call().entity(StudentRecord.class);
}
}


对话记忆ChatMemory

一句话总结:Spring AI Alibaba 中的聊天记忆提供了维护 AI 聊天应用程序的对话上下文和历史的机制。
注意:引入的 redis 是引入的 jedis,因为官方的这个 RedisChatMemoryRepository 是用的 JedisPool。

java
@Configuration
public class RedisMemoryConfig {
@Value("spring.data.redis.host")
private String host;
@Value("spring.data.redis.port")
private int port;
@Bean
public RedisChatMemoryRepository redisChatMemoryRepository() {
return RedisChatMemoryRepository.builder()
.host(host)
.port(port)
.build();
}
}

配置类也有点区别,多加了点东西:
java
@Bean(name = "deepseekChatClient")
public ChatClient deepseekChatClient(@Qualifier("deepseek") ChatModel deepseek,
RedisChatMemoryRepository redisChatMemoryRepository) {
MessageWindowChatMemory windowChatMemory = MessageWindowChatMemory.builder()
.chatMemoryRepository(redisChatMemoryRepository)
.maxMessages(10)
.build();
return ChatClient.builder(deepseek)
.defaultOptions(ChatOptions.builder().model(DEEPSEEK_MODEL).build())
.defaultAdvisors(MessageChatMemoryAdvisor.builder(windowChatMemory).build())
.build();
}
@Bean(name = "qwenChatClient")
public ChatClient qwenChatClient(@Qualifier("qwen") ChatModel qwen,
RedisChatMemoryRepository redisChatMemoryRepository) {
MessageWindowChatMemory windowChatMemory = MessageWindowChatMemory.builder()
.chatMemoryRepository(redisChatMemoryRepository)
.maxMessages(10)
.build();
return ChatClient.builder(qwen)
.defaultOptions(ChatOptions.builder().model(QWEN_MODEL).build())
.defaultAdvisors(MessageChatMemoryAdvisor.builder(windowChatMemory).build())
.build();
}



通以万向-文生图
java
@RestController
public class Text2ImageController {
public static final String IMAGE_MODEL = "wanx2.1-t2i-turbo";
@Resource
private ImageModel imageModel;
@GetMapping(value = "/t2i/image")
public String image(@RequestParam(name = "prompt", defaultValue = "考拉") String prompt) {
return imageModel.call(
new ImagePrompt(prompt, DashScopeImageOptions.builder().withModel(IMAGE_MODEL).build())
)
.getResult()
.getOutput()
.getUrl();
}
}


语音合成-文生音
java
@RestController
public class Text2VoiceController {
@Resource
private SpeechSynthesisModel speechSynthesisModel;
// voice model
private static final String BAILIAN_VOICE_MODEL = "cosyvoice-v2";
// voice timber 音色列表
public static final String BAILIAN_VOICE_TIMBER = "longyingxiao";
@GetMapping("/t2v/voice")
public String voice(@RequestParam(name = "msg", defaultValue = "温馨提醒,支付宝到账100元请注意查收") String msg) {
String filePath = "C:\\try\\" + UUID.randomUUID() + ".mp3";
// 1 语音参数设置
DashScopeSpeechSynthesisOptions options = DashScopeSpeechSynthesisOptions.builder()
.model(BAILIAN_VOICE_MODEL)
.voice(BAILIAN_VOICE_TIMBER)
.build();
// 2调用大模型语音生成对象
SpeechSynthesisResponse response = speechSynthesisModel.call(new SpeechSynthesisPrompt(msg, options));
// 3.字节流语音转换
ByteBuffer byteBuffer = response.getResult().getOutput().getAudio();
// 4.文件生成
try (FileOutputStream fileOutputStream = new FileOutputStream(filePath)) {
fileOutputStream.write(byteBuffer.array());
} catch (Exception e) {
System.out.println(e.getMessage());
}
return filePath;
}
}

向量化和向量数据库
向量:用于表示具有大小和方向的量。
嵌入模型(Embedding Model)
嵌入(Embedding)的工作原理:将文本、图像和视频转换为向量(Vectors)的浮点数数组。
向量数据库是一种专门用于存储、管理和检索向量数据(即高维数值数组)的数据库系统。其核心功能是通过高效的索引结构 和相似性计算算法,支持大规模向量数据的快速查询和分析,向量数据库维度越高,查询精准度也越高,查询效果也越好。
新技术 Redis Stack
Redis Stack 是 Redis Labs 推出的一个"增强版 Redis",不是 Redis 的替代品,而是 在原生 Redis 基础上的功能扩展包,专为构建现代实时应用而设计。

一句话总结:RedisStack = 原生 Redis + 搜索 + 图 + 时间序列 + JSON + 概率结构 + 可视化工具 + 开发框架支持
Redis Stack 安装(基于 Docker):
bash
docker run -d --name redis-stack-server -p 6380:6379 redis/redis-stack-server
配置文件:
properties
server.port=8011
# 设置响应的字符编码
server.servlet.encoding.charset=utf-8
server.servlet.encoding.enabled=true
server.servlet.encoding.force=true
spring.application.name=SAA-11Embed2vector
# ====SpringAIAlibaba Config=============
spring.ai.dashscope.api-key=${DASHSCOPE_API_KEY}
spring.ai.dashscope.chat.options.model=qwen-plus
spring.ai.dashscope.embedding.options.model=text-embedding-v3
# =======Redis Stack==========
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.username=default
# spring.data.redis.password=
spring.ai.vectorstore.redis.initialize-schema=true
spring.ai.vectorstore.redis.index-name=custom-index
spring.ai.vectorstore.redis.prefix=custom-prefix
文本向量化:
java
@RestController
@Slf4j
public class Embed2VectorController {
@Resource
private EmbeddingModel embeddingModel;
@Resource
private VectorStore vectorStore;
/**
* 文本向量化
* http://localhost:8011/text2embed?msg=射雕英雄传
*/
@GetMapping("/text2embed")
public EmbeddingResponse text2Embed(String msg) {
// EmbeddingResponse embeddingResponse = embeddingModel.call(new EmbeddingRequest(List.of(msg), null));
EmbeddingResponse embeddingResponse = embeddingModel.call(new EmbeddingRequest(
List.of(msg),
DashScopeEmbeddingOptions.builder().withModel("text-embedding-v3").build())
);
System.out.println(Arrays.toString(embeddingResponse.getResult().getOutput()));
return embeddingResponse;
}
}

向量化存储:
java
/**
* 文本向量化后存入向量数据库 RedisStack
*/
@GetMapping("/embed2vector/add")
public void add(){
List<Document> documents = List.of(
new Document("I study LLM"),
new Document("I love java"));
vectorStore.add(documents);
}

Redis 里查看:

向量化查询:
java
/**
* 从向量数据库RedisStack 查找,进行相似度查找
* http://localhost:8011/embed2vector/get?msg=LLM
*/
@GetMapping("/embed2vector/get")
public List getAll(@RequestParam(name = "msg") String msg) {
SearchRequest searchRequest = SearchRequest.builder().query(msg).topK(2).build();
List<Document> list = vectorStore.similaritySearch(searchRequest);
System.out.println(list);
return list;
}

RAG
Retrieval Augmented Generation:检索增强生成。
用的 Spring AI + 阿里百炼嵌入模型 text-embedding-v3 + 向量数据库 RedisStack + DeepSeek 来实现 RAG 功能。
LLM 的缺陷:
① LLM 的知识不是实时的,不具备知识更新
② LLM 可能不知道你私有的领域/业务知识
③ LLM 有时会在回答中生成看似合理但实际上是错误的信息
RAG 的核心设计理念:RAG 技术就像给 AI 大模型装上了 「实时百科大脑」,为了让大模型获取足够的上下文,以便获得更加广泛的信息源,通过先查资料后回答的机制,让 AI 摆脱传统模型的"知识遗忘和幻觉回复"困境。就类似考试时有不懂的,给你准备了小抄,对大模型知识盲区的一种补充。
RAG 允许模型在生成答案之前,从特定的知识库中检索相关信息,从而提供更准确和上下文相关的回答。
RAG 过程分为两个阶段:索引(indexing)和检索(retrieval)。
索引(Indexing):索引首先清理和提取 各种格式的原始数据,如 PDF、HTML、Word 和 Markdown,然后将其转换为统一的纯文本格式 。为了适应语言模型的上下文限制,文本被分割成更小的、可消化的块(Chunk)。然后使用嵌入模型 将块编码成向量表示,并存储在向量数据库中。这一步对于在随后的检索阶段实现高效的相似度搜索至关重要。知识库分割成 chunks,并将 chunks 向量化至向量库中。
检索(Retrieval):在收到用户查询(Query)后,RAG 系统采用与索引阶段相同的编码模型 将查询转换为向量表示,然后计算索引语料库中查询向量 与块向量的相似性得分。该系统优先级和检索最高 k(Top-K)块,显示最大的相似性查询。
他课程给的一个小例子:一个 AI 智能运维助手,通过提供的错误编码,给出异常解释辅助运维人员更好的定位问题和维护系统。
java
@Configuration
public class InitVectorDatabaseConfig
{
@Autowired
private VectorStore vectorStore;
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Value("classpath:ops.txt")
private Resource opsFile;
@PostConstruct
public void init()
{
//1 读取文件
TextReader textReader = new TextReader(opsFile);
textReader.setCharset(Charset.defaultCharset());
//2 文件转换为向量(开启分词)
List<Document> list = new TokenTextSplitter().transform(textReader.read());
//3 写入向量数据库RedisStack
//vectorStore.add(list);
// 解决上面第3步,向量数据重复问题,使用redis setnx命令处理
//4 去重复版本
String sourceMetadata = (String)textReader.getCustomMetadata().get("source");
String textHash = SecureUtil.md5(sourceMetadata);
String redisKey = "vector-xxx:" + textHash;
// 判断是否存入过,redisKey如果可以成功插入表示以前没有过,可以假如向量数据
Boolean retFlag = redisTemplate.opsForValue().setIfAbsent(redisKey, "1");
System.out.println("****retFlag : "+retFlag);
if(Boolean.TRUE.equals(retFlag))
{
//键不存在,首次插入,可以保存进向量数据库
vectorStore.add(list);
}else {
//键已存在,跳过或者报错
//throw new RuntimeException("---重复操作");
System.out.println("------向量初始化数据已经加载过,请不要重复操作");
}
}
}
java
@RestController
public class RagController {
@Resource(name = "qwenChatClient")
private ChatClient chatClient;
@Resource
private VectorStore vectorStore;
/**
* http://localhost:8012/rag4aiops?msg=00000
* http://localhost:8012/rag4aiops?msg=c2222
*/
@GetMapping("/rag4aiops")
public Flux<String> rag(String msg) {
String systemInfo = """
你是一个运维工程师,按照给出的编码给出对应的故障解释,否则就回复找不到信息。
""";
RetrievalAugmentationAdvisor advisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(vectorStore).build())
.build();
return chatClient.prompt().system(systemInfo).user(msg).advisors(advisor).stream().content();
}
}

ToolCalling
不调用 ToolCalling 会发生什么?

ToolCalling 相当于是 LLM 的外部 util 工具类。
ToolCalling(也称为 Function Calling),它允许大模型与一组 API 或工具进行交互,将 LLM 的智能与外部工具或 API 无缝连接,从而增强大模型的功能。
LLM 本身并不执行函数,它只是指示应该调用哪个函数以及如何调用。
MCP(Model Context Protocol) 上下文协议可以理解为就是对 ToolCalling 工具调用的一种增强。

不用ToolCalling
java
@RestController
public class NoToolCallingController {
@Resource
private ChatModel chatModel;
// http:localhost:8013/notoolcall/chat
@GetMapping("/notoolcall/chat")
public Flux<String> chat(@RequestParam(name = "msg", defaultValue = "你是谁现在几点")String msg) {
return chatModel.stream(msg);
}
}

使用ToolCalling:
先写个工具类:
java
public class DateTimeTools {
@Tool(description = "获取当前时间", returnDirect = true)
public String getCurrentTime() {
return LocalDateTime.now().toString();
}
}
java
@RestController
public class ToolCallingController {
@Resource
private ChatModel chatModel;
// http:localhost:8013/toolcall/chat
@GetMapping("/toolcall/chat")
public String chat(@RequestParam(name = "msg", defaultValue = "你是谁现在几点")String msg){
// 1、工具注册到工具集合里
ToolCallback[] tools = ToolCallbacks.from(new DateTimeTools());
// 2、将工具集配进ChatOption对象
ChatOptions options = ToolCallingChatOptions.builder().toolCallbacks(tools).build();
// 3、构建提示词
Prompt prompt = new Prompt(msg, options);
// 4、调用大模型
return chatModel.call(prompt).getResult().getOutput().getText();
}
}

java
@Resource
private ChatClient chatClient;
@GetMapping("/toolcall/chat2")
public Flux<String> chat2(@RequestParam(name = "msg", defaultValue = "你是谁现在几点") String msg) {
return chatClient.prompt(msg)
.tools(new DateTimeTools())
.stream()
.content();
}

总结
- 新建一个 Tool 工具类
ChatModel/ChatClient的使用ToolCalling使用的注意事项:ToolCalling使用的前提是大模型支持FunctionCalling才能正常调用(当然一般的都支持)。
MCP
Model Context Protocol(MCP):模型上下文协议。

Java 里面的 SpringCloud Openfeign,只不过 Openfeign 是用于微服务通讯的,而 MCP 是用于大模型通讯的,但它们都是为了获取某项数据的一种机制。
所谓分久必合,合久必分。

MCP 遵循客户-服务器 架构(C-S架构),包含以下几个核心部分:
-
MCP 提供了一种标准化的方式来连接 LLMs 需要的上下文,MCP 就类似于一个 Agent 时代的 Type-C 协议,希望能将不同来源的数据、工具、服务统一起来供大模型调用。
-
MCP 主机(MCP Hosts):发起请求的 AI 应用程序,比如聊天机器人、AI 驱动的 IDE 等。
-
MCP 客户端(MCP Clients):在主机程序内部,与 MCP 服务器保持 1:1 的连接。
-
MCP 服务器(MCP Servers):为 MCP 客户端提供上下文、工具和提示信息。
-
本地资源(Local Resoures):本地计算机中可供 MCP 服务器安全访问的资源,如文件、数据库
-
远程资源(Remote Resources):MCP 服务器可以连接到的远程资源,如通过 API 提供的数据
在 MCP 通信协议中,一般有两种模式:
STDIO(标准输入/输出):支持标准输入和输出流进行通信,主要用于本地集成、命令行工具等场景SSE(Server-Sent-Events):支持使用 HTTP POST 请求服务器到客户端流式处理,以实现客户端到服务器的通信

总结:
- ToolCalling:工具类,为了让大模型使用 util 工具
- RAG:知识库,为了让大模型获取足够的上下文
- MCP:协议,为了大模型之间的相互调用
之前每个大模型如 DeepSeek、ChatGPT 需要为每个工具单独开发接口 Function Calling,导致重复劳动。
而开发者只需要一次 MCP,服务端所有兼容 MCP 的协议都能调用 MCP,让大模型从"被动应答"变为"主动调用工具"。
我调用一个 MCP 服务器就等价于调用一个带有多个功能的 Utils 工具类,自己还不用受累携带。↔️MCP统一协议
MCP之本地 Server 服务端实现:
xml
<dependencies>
<!--注意事项(重要)
spring-ai-starter-mcp-server-webflux不能和 <artifactId>spring-boot-starter-web</artifactId> 依赖并存,
否则会使用tomcat启动,而不是netty启动,从而导致mcpserver启动失败,但程序运行是正常的,mcp客户端连接不上。
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--mcp-server-webflux-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

java
@Configuration
public class McpServerConfig {
// 将工具方法暴露给外部的 mcp client 调用
@Bean
public ToolCallbackProvider weatherTools(WeatherService weatherService) {
return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
}
}
java
@Service
public class WeatherService {
@Tool(description = "根据城市名称获取天气预报")
public String getWeatherByCity(String city) {
Map<String, String> map = Map.of(
"北京", "11111降雨频繁,其中今天和后天雨势较强,部分地区有暴雨并伴强对流天气,需注意",
"上海", "22222多云,15℃~27℃,南风3级,当前温度27℃。",
"深圳", "333333多云40天,阴16天,雨30天,晴3天"
);
return map.getOrDefault(city, "抱歉:未查询到对应城市!");
}
}

MCP 之本地 Client 客户端实现:
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring-ai-alibaba dashscope-->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<!-- 2.mcp-clent 依赖 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
yaml 文件:
properties
server.port=8015
server.servlet.encoding.enabled=true
server.servlet.encoding.force=true
server.servlet.encoding.charset=UTF-8
spring.application.name=SAA-15LocalMcpClient
# ====SpringAIAlibaba COnfig=====
spring.ai.dashscope.api-key=${DASHSCOPE_API_KEY}
# ====mcp-client Config=============
spring.ai.mcp.clent.type=async
spring.ai.mcp.client.request-timeout=60s
spring.ai.mcp.client.toolcallback.enabled=true
# 这个关键
spring.ai.mcp.client.sse.connections.mcp-server1.url=http://localhost:8014
java
@Configuration
public class SaaLLMConfig {
@Bean
public ChatClient chatClient(ChatModel chatModel, ToolCallbackProvider tools) {
return ChatClient.builder(chatModel)
.defaultToolCallbacks(tools.getToolCallbacks()) // mcp协议,配置见 yml 文件
.build();
}
}
java
@RestController
public class McpClientController {
@Resource
private ChatClient chatClient; // 使用了mcp协议支持的(我们在配置类配了的)
@Resource
private ChatModel chatModel; // 没有纳入tool支持,普通调用
@GetMapping("/mcpclient/chat")
public Flux<String> chat(@RequestParam(name = "msg", defaultValue = "北京") String msg) {
System.out.println("使用了 mcp");
return chatClient.prompt(msg).stream().content();
}
@RequestMapping("/mcpclient/chat2")
public Flux<String> chat2(@RequestParam(name = "msg", defaultValue = "北京") String msg) {
System.out.println("未使用mcp");
return chatModel.stream(msg);
}
}

编译器后台也输出了:

以上就是本地玩了一下 MCP。
下面是远程的 MCP 增强案例-对接互联网通用 MCP 服务(百度地图)。
这个很重要,要动手练习。
Baidu Map MCP Server:对接互联网通用的 MCP 服务(百度地图)。

控制台 | 百度地图开放平台先做一个认证。

java
@Configuration
public class SaaLLMConfig {
@Bean
public ChatClient chatClient(ChatModel chatModel, ToolCallbackProvider tools) {
return ChatClient.builder(chatModel)
// mcp 协议,配置见yml文件,此处只赋能给ChatClient对象
.defaultToolCallbacks(tools.getToolCallbacks())
.build();
}
}
java
@RestController
public class McpClientCallBaiDuMcpController {
@Resource
private ChatClient chatClient; // 添加了MCP的调用能力
@Resource
private ChatModel chatModel; // 没有添加MCP的调用能力
/**
* 添加了 MCP 调用能力
* http://localhost:8016/mcp/chat?msg=查询北纬39.9042东经116.4074天气
* http://localhost:8016/mcp/chat?msg=查询61.149.121.66归属地
* http://localhost:8016/mcp/chat?msg=查询昌平到天安门内的路线规划
*/
@GetMapping("/mcp/chat")
public Flux<String> chat(String msg) {
return chatClient.prompt(msg).stream().content();
}
/**
* 没有添加 MCP 调用能力
* http://localhost:8016/mcp/chat?msg=查询北纬39.9042东经116.4074天气
*/
@RequestMapping("/mcp/chat2")
public Flux<String> chat2(String msg) {
return chatModel.stream(msg);
}
}
properties
server.port=8016
# 设置全局编码格式
server.servlet.encoding.enabled=true
server.servlet.encoding.force=true
server.servlet.encoding.charset=UTF-8
spring.application.name=SSA-16ClientCallBaiduMcpServer
# ====LLM Config=============
spring.ai.dashscope.api-key=${DASHSCOPE_API_KEY}
# ====mcp-client Config=============
spring.ai.mcp.client.request-timeout=20s
spring.ai.mcp.client.toolcallback.enabled=true
spring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-server.json5

使用 ChatClient 去访问:

ChatModel 去查询(没有 MCP):

MCP源码小分析

SAA生态-AI智能运维



java
@Configuration
public class DashScopeConfig {
@Value("${spring.ai.dashscope.api-key}")
private String apiKey;
@Bean
public DashScopeApi dashScopeApi(){
return DashScopeApi.builder().apiKey(apiKey)
.build();
}
@Bean
public ChatClient chatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}
}
java
@Controller
public class BailianController {
@Resource
private ChatClient chatClient;
@Resource
private DashScopeApi dashScopeApi;
/**
* http:localhost:8017/bailian/rag/chat
* http:localhost:8017/bailian/rag/chat?msg=A0001
*/
@GetMapping("/bailian/rag/chat")
public Flux<String> chat(@RequestParam(name = "msg", defaultValue = "00000错误信息") String msg) {
DocumentRetriever retriever = new DashScopeDocumentRetriever(dashScopeApi,
DashScopeDocumentRetrieverOptions.builder()
.withIndexName("ops") // 知识库名称
.build());
return chatClient.prompt()
.user(msg)
.advisors(new DocumentRetrievalAdvisor(retriever))
.stream()
.content();
}
}

SAA生态-今天吃什么
不用SAA生态
properties
server.port=8018
server.servlet.encoding.enabled=true
server.servlet.encoding.force=true
server.servlet.encoding.charset=UTF-8
spring.application.name=SAA-18TodayMenu
# ====SpringAIAlibaba Config=============
spring.ai.dashscope.api-key=${DASHSCOPE_API_KEY}
java
@Configuration
public class DashScopeConfig {
@Value("${spring.ai.dashscope.api-key}")
private String apiKey;
@Bean
public DashScopeApi dashScopeApi() {
return DashScopeApi.builder()
.apiKey(apiKey)
.build();
}
@Bean
public ChatClient chatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}
}
java
@RestController
public class MenuController {
@Resource
private ChatModel chatModel;
@GetMapping(value = "/eat")
public Flux<String> eat(@RequestParam(name = "msg", defaultValue = "今天吃什么") String question) {
String info = """
你是一个AI厨师助手,每次随机生成三个家常菜,并
且提供这些家常菜的详细做法步骤,以HTML格式返回,字数控制在1500字以内。
""";
// 系统消息
SystemMessage systemMessage = new SystemMessage(info);
// 用户消息
UserMessage userMessage = new UserMessage(question);
Prompt prompt = new Prompt(userMessage, systemMessage);
return chatModel.stream(prompt).mapNotNull(response -> response.getResults().getFirst().getOutput().getText());
}
}

用SAA生态:
先在平台上配置:





配置类保持原样,主要是在 Controller 类里面加:
java
@RestController
public class MenuCallAgentController {
// 百炼云平台的智能体接口对象
@Value("${spring.ai.dashscope.agent.options.app-id}")
private String appId;
// 百炼云平台的智能体接口对象
private DashScopeAgent dashScopeAgent;
public MenuCallAgentController(DashScopeAgentApi dashScopeAgentApi) {
this.dashScopeAgent = new DashScopeAgent(dashScopeAgentApi);
}
// http://localhost:8018/eatAgent
@GetMapping(value = "/eatAgent")
public String eatAgent(@RequestParam(name = "msg", defaultValue = "今天吃什么") String msg) {
DashScopeAgentOptions options = DashScopeAgentOptions.builder().withAppId(appId).build();
Prompt prompt = new Prompt(msg, options);
return dashScopeAgent.call(prompt).getResult().getOutput().getText();
}
}

RAG检索增强生成
为什么需要RAG?
2025大模型RAG入门第一课(01)------为什么需要RAG_哔哩哔哩_bilibili
有一句话:做好 RAG 只要一星期,但是上线要一年。
大模型的缺陷(除了幻觉、知识更新不及时等)之一:它天然无法回答私域问题,所以就无法深入地用于企业。
大模型所有地知识是来自于它的训练数据,训练数据以外的知识它是不知道的。
而大模型主要的训练数据是来自于公开的互联网。
而专家的消息说:目前能用的数据都被用光了。
RAG:简单来说,就是根据给定的问题,从知识库中检索出合适的参考内容,让大模型据此回答。
可以发现 RAG 有三个核心:知识库、检索、回答。
RAG的基本流程
2025大模型RAG入门第一课(02)------RAG的基本流程:为什么说上手RAG只要一星期_哔哩哔哩_bilibili

准备知识库
-
知识收集

-
切分文档

切分文档的方法:
具体怎么切分,没有固定的套路,核心是要保障语义的连贯性。

-
文本向量
为了让计算机能够处理文字,要将文字转为向量。问题是,怎么转化?
分为稀疏向量和稠密向量(基于词嵌入):

词嵌入:

-
向量存储
为向量存储和处理而生的数据库:向量数据库。

现在就在向量数据库(知识库)中找到相应的文档,这里介绍 2 种方法,分别是基于文本相似度的检索 和 基于关键字的检索。
-
基于文本相似度的检索
现在是有问题向量 ,要找到知识块向量。

-
基于关键字的检索


至于如何将这两种方法结合起来,这个在优化阶段我们会提到。
生成阶段
包括提示词的构造 ,模型的选择。

总结

RAG的优化技巧
2025大模型RAG入门第一课(03)------RAG的10个优化技巧:为什么RAG不是算法问题,是工程问题_哔哩哔哩_bilibili

RAG 不是算法问题,而是工程问题。

5 种检索方案的优化
- 3 种
small to big的方法

-
多路召回

生成优化阶段的优化
这个阶段的优化是指:如何利用众多的参考文档。
默认的做法是拿到参考文档后就直接堆叠,丢到 LLM 里面去。
但是会产生 2 个问题:
① 上下文溢出
② 知识块不连贯
介绍下面的 3 种优化方式:

下一种优化:改写提问

最后一个优化方案:合理利用元数据

RAG的评估方法
2025大模型RAG入门第一课(04)------RAG的评估方法:没有评估,就没法优化_哔哩哔哩_bilibili
先介绍几个概念:
准确率:直接站在用户视角,看答案是否符合实际情况。
忠实度:生成的内容是否忠实于提供的上下文或背景信息。就是说我已经准确检索出了相关的参考文档,把提问和参考文档一起给大模型,大模型的答复有没有依据这个参考文档来给出。若有,忠实度就高;没有,忠实度就不行,大模型就得替换。
召回率、精确率、F1。这三个指标是评估参考文档有没有准确、完整的找出来。
下图中,灰色表示进行知识库建设的时候,所有的知识块 A。
蓝色:在某一次检索的时候,所有相关的知识块。
绿色:实际检索出来的参考文档 B。
蓝色与绿色的交集:你找到的并且准确的 C。
召回率 = C / A,指的就是你有没有把所有可能相关的参考文档尽可能多的找出来。
精确率 = C / B,中间的交集 除以 检索出来的

F1 是对召回率 和精确率的一个综合评估:

RAG 的评估方法:
- 人工评估
- 模型评估

RAG代码实践
RAG 工作机制详解------一个高质量知识库背后的技术全流程_哔哩哔哩_bilibili
说白了,RAG 就做下面 2 件事:
- 先从资料库里检索相关内容
- 再基于这些内容来生成答案
顾名思义:检索 增强生成
RAG 是目前最常用的 AI 问答方案之一。很多企业内的智能助手,智能客服用的都是这项技术。
RAG的基本运行流程

分片

可以访问:Spaces - Hugging Face 看看 Embedding 的排行。
向量数据库:
向量只是一个中间结果。
![]()
索引
通过 Embedding 将每个片段文本转换为→向量,并且把片段文本和对应的向量存入向量数据库的过程。
分片、索引都发生在用户提问之前,属于要提前准备的步骤。
下面就来看看,用户提问之后发生了什么?
召回(Recall)
就是搜索与用户问题相关片段的过程。

当然,10 这个数字并不固定,也可以选择 15、20 等,具体是多少不重要,只要不是很多都可以。

那么 similarity 这个公式是如何计算出来的呢?
有很多种。目前比较流行的方案是:余弦相似度 、欧式距离 、点积。
回到上上张图,我们查询出了与用户问题最匹配的 10 个片段。然后就把这 10 个片段发送到重排阶段继续处理。
重排(Rerank)
重排,全称是重新排序。它做的事情其实跟召回是一样的。
重排就是在刚刚召回得到的这 10 份里面,再挑 3 份与用户问题最相似的,作为重排的结果。
你可能会想,那直接在召回阶段挑 3 份不就好了吗?同样的事情搞两边干什么呢?
一次挑出 3 个当然是可以的,不过这样做的效果没有**「召回 + 重排」**好。

生成
重排 结束后,就进入了生成阶段。

RAG 的整体流程总结

使用Python构建RAG系统
使用Python构建RAG系统 ------ 用代码还原 RAG系统的每个细节_哔哩哔哩_bilibili
按照「马克的技术工作坊」Up 主的视频教程,先安装 uv 环境:
不需要单独安装 Jupyter,uv 会自动处理。
放这个项目的路径:
bash
C:\study\Rag_Python_Project_Mark
然后在项目根目录下创建一个名为 .env 的文件,并添加以下内容:
bash
GEMINI_API_KEY=xxx

其中 xxx 为你的 Google Gemini API 密钥。没有密钥的用户可以在 https://aistudio.google.com/apikey 上申请。
然后使用 uv 安装如下 Python 依赖:
bash
uv add sentence_transformers chromadb google-genai python-dotenv

安装好 python 依赖后,使用 uv 运行 Jupyter Notebook:
bash
uv run --with jupyter jupyter lab


直接把他的文件 main.ipynb 导进来:





最后运行,好像说要 python 的版本为 3.12,而我是 3.11,所以有点问题。
Agent开发
Function Calling & MCP 协议
什么是Function Calling与MCP协议?它们为何要这样设计?_哔哩哔哩_bilibili
看她自带的笔记吧。
https://oigi8odzc5w.feishu.cn/wiki/LWqEwXNkBibT0ykrbI0cvptBnAf 密码:4892@u29
下面的 MCP 三节内容,是看的「马克的技术工作坊」的视频笔记。
MCP基础篇
看的这个视频:
MCP:Model Context Protocol(模型上下文协议),Anthropic 公司在 2024 年 11 月 25 号发布的一个协议。
简单来说:MCP 就是能让大模型更好地使用各类工具的一个协议。
大模型本身只会问答,它是不能使用工具的。而 MCP 的出现,就让大模型拥有了使用各种外部工具的能力。
而要用 MCP,还得使用 MCP Host ,它本质上就是一个支持 MCP 协议的软件。常见的 MCP Host 包括:Claude Desktop、Cursor、Cline(VS Code 的插件,后面课程基于这个)、Cherry Studio 等。

接下来,配置 Cline 中用的 API Key。
他这里用的是 Open Router,API Keys | Settings | OpenRouter,Open Router 提供了很多模型可供选用(但是价格贵),几乎覆盖了所有的主流模型,甚至有些可以免费用。
先登录,再创建一个 key,因为我们使用免费的模型,所以获取到 key 后,操作就结束。
我这里是直接用的 DeepSeek 的 API Key,因为充了点钱的。

MCP Server
MCP 服务器,听着高大上,而且似乎与我们传统网络协议里的 Server 有点联系,似乎是一个远程的服务器,必须联网才能使用?
不过,实际上,MCP Server 跟我们传统意义上的 Server 并没有多大的关系。它就是一个程序而已,只不过这个程序的执行是符合 MCP 协议的。
不管联不联网,他都可以叫做 MCP Server。所以他觉得 「MCP Server」 这个名字里面,带 Server 这个词是有一定误导性的。
不必觉得这玩意很高端,很玄妙。它本质上就是一个程序。就像手机应用一样。

比如一个 获取天气的 MCP Server,就会包含两个 Tool。

MCP Server 内部交互流程

如何使用他人制作的 MCP Server ?uvx 部分。
目前有很多的 MCP Server 市场,比如:
html
mcp.so
mcpmarket.com
smithery.ai
MCP Server 大多是使用 Python 或者 Node 进行编写的,对应的启动程序一般是 uvx 或者 npx。

对于这 2 种启动方式,我们分别举一个例子,其他的大家举一反三就好。
-
uvx,uvx是uv tool run这个命令的缩写,代表使用uv来运行一个Tool。(不过这里的Tool,是uv领域的Tool,跟 MCP 的Tool不是一个东西)uv,是 Python 里的一个包管理软件,而其中的uvx,可以用来直接启动 Python 程序。
比如,
bashuvx ruff这个命令 可以用来安装并启动
ruff这个程序。uvx会帮你把ruff需要的依赖,执行环境全部都配置好,不需要自己去处理。而使用
uvx之前,需要安装它(自己之前已经安装过了)。

在 mcp.so 这个网站搜索 fetch。

安装好 fetch 这个 MCP Server 后,就可以来用用看。
首先打开一个新的会话,输入问题:
bash
请抓取下面这个网页的内容,并将其转为 markdown 后放到项目目录里面的 guides.md 文件中:
https://docs.astral.sh/uv/guides/install-python/
这个网站的内容是:

-
再来看另一种启动 MCP Server 的方式:
npx。npx跟uvx类似,也是可以自动下载并且安装程序。只不过uvx是安装的 python 程序,而npx安装的是 Node 程序。由于
npx是 Node 的一部分,所以我们直接安装 Node.js 就行(自己已经安装过了)。打开
mcpmarket.com这个网站,搜索HotNews。

MCP进阶篇
进阶篇大致分为 3 部分:
- 手写一个 MCP Server
- 截获 MCP Server 的输入和输出,并逐行分析(包括不借助 MCP Host 和任何编程语言的情况下,直接与 MCP Server 沟通)
- 掌握了协议细节后,回头再想想 MCP 到底是个什么意思。它在大模型相关的应用中,到底扮演了什么角色?
MCP终极指南 - 带你深入掌握MCP(进阶篇)_哔哩哔哩_bilibili
这里他使用 Python 来讲解.
自己重新安装更新版本的 Python ,在官网下载的:Python Release Python 3.14.5 | Python.org
除了 Python 以外,还需要三样东西:uv(Python 的包管理器),VSCode、Cline(VSCode插件)。
下面开始讲解。
为方便复制粘贴代码,他使用的官方的例子:Build an MCP server - Model Context Protocol
他写的代码与官方基本相同,只是有点小改动。
bash
uv init weather
cd weather
uv venv
.venv\Scripts\activate
uv add "mcp[cli]" httpx --index-url https://pypi.tuna.tsinghua.edu.cn/simple

依赖安装完成后,用 VSCode 打开 weather 这个项目目录,再新建一个 weather.py 的文件(我们主要的工作就在这个文件中完成):

weather.py 的内容:
python
from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP
# Initialize FastMCP server
mcp = FastMCP("weather", log_level="ERROR")
# Constants
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"
async def make_nws_request(url: str) -> dict[str, Any] | None:
"""Make a request to the NWS API with proper error handling."""
headers = {
"User-Agent": USER_AGENT,
"Accept": "application/geo+json"
}
async with httpx.AsyncClient() as client:
try:
response = await client.get(url, headers=headers, timeout=30.0)
response.raise_for_status()
return response.json()
except Exception:
return None
def format_alert(feature: dict) -> str:
"""Format an alert feature into a readable string."""
props = feature["properties"]
return f"""
Event: {props.get('event', 'Unknown')}
Area: {props.get('areaDesc', 'Unknown')}
Severity: {props.get('severity', 'Unknown')}
Description: {props.get('description', 'No description available')}
Instructions: {props.get('instruction', 'No specific instructions provided')}
"""
@mcp.tool()
async def get_alerts(state: str) -> str:
"""Get weather alerts for a US state.
Args:
state: Two-letter US state code (e.g. CA, NY)
"""
url = f"{NWS_API_BASE}/alerts/active/area/{state}"
data = await make_nws_request(url)
if not data or "features" not in data:
return "Unable to fetch alerts or no alerts found."
if not data["features"]:
return "No active alerts for this state."
alerts = [format_alert(feature) for feature in data["features"]]
return "\n---\n".join(alerts)
@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
"""Get weather forecast for a location.
Args:
latitude: Latitude of the location
longitude: Longitude of the location
"""
# First get the forecast grid endpoint
points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
points_data = await make_nws_request(points_url)
if not points_data:
return "Unable to fetch forecast data for this location."
# Get the forecast URL from the points response
forecast_url = points_data["properties"]["forecast"]
forecast_data = await make_nws_request(forecast_url)
if not forecast_data:
return "Unable to fetch detailed forecast."
# Format the periods into a readable forecast
periods = forecast_data["properties"]["periods"]
forecasts = []
for period in periods[:5]: # Only show next 5 periods
forecast = f"""
{period['name']}:
Temperature: {period['temperature']}°{period['temperatureUnit']}
Wind: {period['windSpeed']} {period['windDirection']}
Forecast: {period['detailedForecast']}
"""
forecasts.append(forecast)
return "\n---\n".join(forecasts)
if __name__ == "__main__":
# Initialize and run the server
mcp.run(transport='stdio')
一直报错的原因(问的 ClaudeCode + deepseek v4-pro):
问题原因
weather.py第91行的温度符号°是用 GBK编码 (\xa1\xe3)保存的,而Python 3源文件默认要求 UTF-8编码。当Cline尝试启动MCP服务器时,Python在解析源文件阶段就抛出:
SyntaxError: Non-UTF-8 code starting with '\xa1' in weather.py on line 91服务器进程直接崩溃,Cline自然连接不上。
修复方式
将第91行的GBK编码
°→ 替换为UTF-8编码°(即\xa1\xe3→\xc2\xb0)。现在
uv run weather.py可以正常启动了。重新在Cline中连接MCP应该就能工作了。
现在就可使用了:

返回了结果,说明我们写的 MCP Server 是没问题的。
MCP底层协议分析的原理与方法
刚才这个 MCP Server 虽然是自己写的,但是我们其实是用了 MCP 的库来完成它的。这个库帮我们做了很多事情,我们并不清楚它到底做了什么。
需要理解的是:大部分的 MCP Server 都是通过输入和输出与 Cline 沟通的,
若这个 MCP Server 是在我们的终端里面启动的,那我们就可以直接看到它的输入和输出。
只是,这个 MCP Server 是 Cline 启动的,它的输入和输出只有 Cline 才能看到,我们看不到。
于是,我们写一个脚本,我们暂且把这个脚本的名称叫做 mcp_logger.py,这个脚本的参数就是用于启动我们这个 MCP Server 的命令
以刚才的 weather 举例,它原来是用这个命令启动的:
bash
uv --directory C:\\Users\\HWL\\Desktop\\kjisdjidj\\weather run weather.py
现在,在前面加上 python mcp_logger.py:
bash
# mcp_logger.py 的功能:截取后面的 MCP Server 的输入和输出
python mcp_logger.py uv --directory C:\\Users\\HWL\\Desktop\\kjisdjidj\\weather run weather.py

他是直接让 Gemini 2.5 Pro 写了一个。
回到 Cline,继续编辑配置:

这时候,Cline 就跟我们的 MCP Server 有一些交互了,输入输出是写到了同目录下的 mcp_io.log 文件里:
每行最前面的输入、输出和 :,是 mcp_logger.py 这个脚本加的,不属于 Cline 与 MCP 的交互内容。他们的交互内容就是后面的 JSON。
- 输入:Cline → MCP Server
- 输出:MCP Server → Cline
用 Claude Code 做了下格式化,方便阅读。
格式化后:
输入 --- initialize · id: 0
json
{
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": {
"name": "Cline",
"version": "3.85.0"
}
},
"jsonrpc": "2.0",
"id": 0
}
输出 --- id: 0
json
{
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersion": "2025-11-25",
"capabilities": {
"experimental": {},
"prompts": { "listChanged": false },
"resources": { "subscribe": false, "listChanged": false },
"tools": { "listChanged": false }
},
"serverInfo": {
"name": "weather",
"version": "1.27.1"
}
}
}
输入 --- notifications/initialized
json
{
"method": "notifications/initialized",
"jsonrpc": "2.0"
}
输入 --- tools/list · id: 1
json
{
"method": "tools/list",
"jsonrpc": "2.0",
"id": 1
}
输出 --- id: 1
json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "get_alerts",
"description": "Get weather alerts for a US state.\n\n Args:\n state: Two-letter US state code (e.g. CA, NY)\n ",
"inputSchema": {
"properties": {
"state": { "title": "State", "type": "string" }
},
"required": ["state"],
"title": "get_alertsArguments",
"type": "object"
},
"outputSchema": {
"properties": {
"result": { "title": "Result", "type": "string" }
},
"required": ["result"],
"title": "get_alertsOutput",
"type": "object"
}
},
{
"name": "get_forecast",
"description": "Get weather forecast for a location.\n\n Args:\n latitude: Latitude of the location\n longitude: Longitude of the location\n ",
"inputSchema": {
"properties": {
"latitude": { "title": "Latitude", "type": "number" },
"longitude": { "title": "Longitude", "type": "number" }
},
"required": ["latitude", "longitude"],
"title": "get_forecastArguments",
"type": "object"
},
"outputSchema": {
"properties": {
"result": { "title": "Result", "type": "string" }
},
"required": ["result"],
"title": "get_forecastOutput",
"type": "object"
}
}
]
}
}
以这个 get_forecast 为例,着重看下:
inputSchema 就是通过之前写的 @mcp.tool() 这个装饰器,从参数里面提取出来的。

输入 --- resources/list · id: 2
json
{
"method": "resources/list",
"jsonrpc": "2.0",
"id": 2
}
输出 --- id: 2
json
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"resources": []
}
}
输入 --- resources/templates/list · id: 3
json
{
"method": "resources/templates/list",
"jsonrpc": "2.0",
"id": 3
}
输出 --- id: 3
json
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resourceTemplates": []
}
}
输入 --- prompts/list · id: 4
json
{
"method": "prompts/list",
"jsonrpc": "2.0",
"id": 4
}
输出 --- id: 4
json
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"prompts": []
}
}
再新开启一个对话(再问他一遍):



输入 --- tools/call · id: 5
json
{
"method": "tools/call",
"params": {
"name": "get_forecast", // 要使用这个 tool
"arguments": { // 参数
"latitude": 40.7128,
"longitude": -74.006
}
},
"jsonrpc": "2.0",
"id": 5
}
输出 --- id: 5
json
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"content": [
{
"type": "text",
"text": "\nToday:\nTemperature: 78°F\nWind: 13 to 22 mph NW\nForecast: Mostly sunny. High near 78, with temperatures falling to around 75 in the afternoon. Northwest wind 13 to 22 mph, with gusts as high as 32 mph.\n\n---\n\nTonight:\nTemperature: 57°F\nWind: 9 to 21 mph NW\nForecast: Mostly clear, with a low around 57. Northwest wind 9 to 21 mph.\n\n---\n\nFriday:\nTemperature: 80°F\nWind: 9 to 14 mph NW\nForecast: Sunny, with a high near 80. Northwest wind 9 to 14 mph.\n\n---\n\nFriday Night:\nTemperature: 59°F\nWind: 15 to 21 mph W\nForecast: Partly cloudy. Low around 59, with temperatures rising to around 61 overnight. West wind 15 to 21 mph.\n\n---\n\nSaturday:\nTemperature: 70°F\nWind: 16 to 22 mph N\nForecast: Mostly sunny. High near 70, with temperatures falling to around 62 in the afternoon. North wind 16 to 22 mph.\n"
}
],
"structuredContent": {
"result": "\nToday:\nTemperature: 78°F\nWind: 13 to 22 mph NW\nForecast: Mostly sunny. High near 78, with temperatures falling to around 75 in the afternoon. Northwest wind 13 to 22 mph, with gusts as high as 32 mph.\n\n---\n\nTonight:\nTemperature: 57°F\nWind: 9 to 21 mph NW\nForecast: Mostly clear, with a low around 57. Northwest wind 9 to 21 mph.\n\n---\n\nFriday:\nTemperature: 80°F\nWind: 9 to 14 mph NW\nForecast: Sunny, with a high near 80. Northwest wind 9 to 14 mph.\n\n---\n\nFriday Night:\nTemperature: 59°F\nWind: 15 to 21 mph W\nForecast: Partly cloudy. Low around 59, with temperatures rising to around 61 overnight. West wind 15 to 21 mph.\n\n---\n\nSaturday:\nTemperature: 70°F\nWind: 16 to 22 mph N\nForecast: Mostly sunny. High near 70, with temperatures falling to around 62 in the afternoon. North wind 16 to 22 mph.\n"
},
"isError": false
}
}
Cline 拿到了 tool 的执行结果后,它与 MCP Server 的交互就算是结束了。所以日志文件到这里也就结束了。
之后,Cline 就会把结果发给大模型,让模型去总结。
我们最后看到的,就是模型总结的结果。
在终端直接与 MCP Server 交互
刚才深挖了 MCP 协议的底层细节,明白了细节后,甚至都不需要一个 MCP Host,就能直接与 MCP Server 沟通。
只需要保证我们发给 MCP Server 的数据符合上面的格式就行。
下面演示下:
用命令(跟前面一样,这次不需要加日志了)启动:
bash
uv --directory C:\\Users\\HWL\\Desktop\\kjisdjidj\\weather run weather.py

可以看到,我们可以直接跟它交互,不需要 MCP Host,或者说我们自己就是 MCP Host。
MCP的真实含义与定位
MCP 协议与模型没啥关系,MCP 协议并没有规定如何与模型进行交互!

MCP 其实只规定了下面这两部分内容:
- 每个 MCP Server 有哪些函数可以用?
- 如何调用这些函数?

以上两点可以总结为 函数的注册与使用。
MCP 规定的是如何发现和调用函数的。这套协议脱离大模型也是能够用的,只是一般没人这么用而已。
MCP 本身并没有规定与模型的交互方式。

实际上,不同的 MCP Host 与模型的交互确实是有很大的差异。比如 Cline 是用 XML 与模型沟通的,而 CheeryStudio 是用 Function Calling 与模型沟通的。
再回过头看 MCP 这个概念,模型上下文协议。什么是上下文,就是环境。环境,就是周围有哪些函数可以用来调用,从而获取到外界的信息。
比如获取到天气信息、网络信息、文件信息等等。
MCP 就是让模型感知外部环境的一个协议,所以叫模型上下文协议。
乍一看,我们很容易理解为这个协议规定的是模型交互的内容,但其实我们最多只能说这个协议是给模型服务的。

MCP番外篇
MCP终极指南 - 番外篇:抓包分析 Cline 与模型的交互协议(内含 Agent 的实现原理)_哔哩哔哩_bilibili
视频中,以 Cline 为例,主要是抓包分析 MCP Host 是如何与模型沟通的。还包含 Agent 常用的 ReAct模式。
![]()
配置 Cline 的本地服务器:

剩下的事,就是编写这个本地服务器,并且确保这个本地服务器的输入和输出符合 OpenAI 的格式规范。
他本地服务器的代码是他直接提供的,用就行了,llm_logger.py。
python
import httpx
from fastapi import FastAPI, Request
from starlette.responses import StreamingResponse
class AppLogger:
def __init__(self, log_file="llm.log"):
"""Initialize the logger with a file that will be cleared on startup."""
self.log_file = log_file
# Clear the log file on startup
with open(self.log_file, 'w') as f:
f.write("")
def log(self, message):
"""Log a message to both file and console."""
# Log to file
with open(self.log_file, 'a') as f:
f.write(message + "\n")
# Log to console
print(message)
app = FastAPI(title="LLM API Logger")
logger = AppLogger("llm.log")
@app.post("/chat/completions")
async def proxy_request(request: Request):
body_bytes = await request.body()
body_str = body_bytes.decode('utf-8')
logger.log(f"模型请求:{body_str}")
body = await request.json()
logger.log("模型返回:\n")
async def event_stream():
async with httpx.AsyncClient(timeout=None) as client:
async with client.stream(
"POST",
"https://openrouter.ai/api/v1/chat/completions",
json=body,
headers={
"Content-Type": "application/json",
"Accept": "text/event-stream", # Server-Sent Events,SSE
"Authorization": request.headers.get("Authorization"),
},
) as response:
async for line in response.aiter_lines():
logger.log(line)
yield f"{line}\n"
return StreamingResponse(event_stream(), media_type="text/event-stream")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
大致流程如图:

Windows 下的命令如下:
bash
python -m venv .venv
.venv\Scripts\activate
pip install -r .\requirements.txt
python llm_logger.py


OpenRouter 必须要充钱,起码 40 元(5$),算了,直接看他视频吧 /_ \
配置完毕后,新开一个对话,发送:Hi。
然后打开日志文件 llm.log:
这个对话没有用到任何 MCP Server 或者外部工具,比较简单,很适合我们先对模型的请求和返回的格式做一下分析。
后面我们还会让模型用下 MCP Server,那个时候的日志内容也会跟这种简单的情况差不多,只是会多出一些调用 MCP Tool 的地方。
简单场景下Cline发往模型的请求

下面就大体过一下 系统提示词(他整理翻译为的中文版):

这里所说的工具,跟 MCP 所说的工具并非完全是一回事。它这里所说的工具一共包含 2 部分的内容,一个是 Cline 内置的工具(比如写入文件、替换文件内容、读取文件、运行中断命令等),还有一个才是 MCP 工具(比如之前的天气预告、气象预警)。
工具的使用格式

工具(Tools)
过一下 Cline 的各种工具:

use_mcp_tool
这个是用来使用 MCP 工具的:

attempt_completion

由于收到了 <attempt_completion>,Cline 就认为本次任务完成了,就结束掉了对话。
已连接的MCP服务器

会把用户在 Cline 上配置好的 MCP 服务器和每个 MCP 服务器所包含的 MCP 工具都列举出来,供模型选用。

以上是 System Prompt,现在来看用户的请求。

系统环境格式化后,看一下:

除了用户请求,剩下的配置部分:

至此,请求的内容就结束了。
简单场景下模型发往Cline的返回

Content 包含的就是模型实际返回的内容,Content 的值是增量返回的,也就是每次返回的都是完整内容的一部分。
把每一行的 Content 的内容拼起来,就可发现,模型实际上返回的就是:

调用MCP工具时模型的请求与返回
前面我们对 Cline 打了个招呼,并分析了下整个链路所发生的事情。
下面来试试让 Cline 调用一个 MCP 工具,并看看日志里面会记录什么。(还是截的视频的图,自己没买 OpenRouter 的 API,所以做不了)
重启中转服务器,
bash
# 跟之前一样的命令,中转服务器每次启动的时候都会自动清空旧的日志文件
python llm_logger.py

新起一个对话,问下经典问题:纽约明天的天气怎么样?

同意调用后:

随后来到日志文件里面:

重点来看下模型的返回:
注释还是一样。重点看 data:

这段 XML 的内容表示:他想要调用 weather 这个MCP 服务器下的 get_forecast 工具,参数是经纬度。
这是工具调用的请求。
Cline 用指定的参数调用了指定的 MCP 服务器之后,会再对模型发起请求。

因为模型没记忆,所以要把前面发生过的事情也告诉模型,方便模型做出判断。

再往下的请求消息:

其实,只有工具的调用结果是新的消息,前面的都是历史消息。
知道了请求内容,就再看下模型到底返回了些啥。

整理后:

Cline的XML协议与React的关系
再来画个流程图:

若在问题后加上一句:把结果写入到 results.md文件中:

实际上,如果我们仔细看下就会发现:在每一轮的工具调用过程中,Cline 与 模型的交互是有固定的模式的。
思考 Thought→ 行动 Action → 观察 Observation → 思考 Thought → 行动 Action → 观察 Observation → ...... → 最终答案

ReAct 思想中最重要的三个词:Thought、Action、Observation。

ReAct 理念,是说在不需要人干预的情况下,让模型自主思考,自主调用外部工具,从而完成用户的诉求。
就像我们的 Cline 一样。说白了,就是一个 Agent。
Agent,就是一种能持续思考,持续调用外部工具,直至解决用户问题的一个程序。
所以从某种意义上来说,Cline 就是一个 Agent,而且本质上,Cline 也是根据 ReAct 的思想来构建他的 Agent 的流程的。

若要改成上图中的「Thought、Action、Observation」这种格式的话,我们也需要一个类似的 System Prompt。
由于 Cline 并未给出修改 System Prompt 的地方,它的格式是写在代码里面的,所以我们实际上没办法让 Cline 按照下面这种格式与模型进行沟通的。
所以下面这个只是一个示例。也就是他给的参考文档:
ReAct系统提示词.md
markdown
你需要解决一个任务。为此,你需要将任务分解为多个步骤。对于每个步骤,首先使用 `Thought:` 思考要做什么,然后使用可用工具之一决定一个 `Action:`。接着,你将根据你的行动从环境/工具中收到一个 `Observation:`。持续这个思考和行动的过程,直到你有足够的信息来提供 `FinalAnswer:`。
这里有一些例子:
---
示例 1:
Question: 埃菲尔铁塔有多高?
Thought: 我需要找到埃菲尔铁塔的高度。可以使用搜索工具。
Action: get_height("埃菲尔铁塔")
Observation: 埃菲尔铁塔的高度约为330米(包含天线)。
Thought: 搜索结果显示了高度。我已经得到答案了。
FinalAnswer: 埃菲尔铁塔的高度约为330米。
---
示例 2:
Question: 帮我找一个简单的番茄炒蛋食谱,并看看家里的冰箱里有没有西红柿。
Thought: 这个任务分两步。第一步,找到番茄炒蛋的食谱。第二步,检查冰箱里是否有西红柿。我先用 `find_recipe` 工具找食谱。
Action: find_recipe(dish="番茄炒蛋")
Observation: 简单的番茄炒蛋食谱:将2个鸡蛋打散,2个番茄切块。热油,先炒鸡蛋,盛出。再热油,炒番茄至软烂,加入鸡蛋,放盐调味即可。
Thought: 好的,我已经有食谱了。食谱需要西红柿。现在我需要用 `check_fridge` 工具看看冰箱里有没有西红柿。
Action: check_fridge(item="西红柿")
Observation: 冰箱检查结果:有3个西红柿。
Thought: 我找到了食谱,并且确认了冰箱里有西红柿。可以回答问题了。
FinalAnswer: 简单的番茄炒蛋食谱是:鸡蛋打散,番茄切块。先炒鸡蛋,再炒番茄,混合后加盐调味。冰箱里有3个西红柿。
---
请严格遵守:
- 输出Action后立即停止生成
- 等待返回真实的Observation
- 擅自生成Observation将导致错误
---
本次任务可用工具:
- get_forecast(latitude, longitude): 获取指定坐标的天气预报。返回包含预报信息的字符串。
- write_to_file(filename, content): 将指定内容写入指定文件。成功时返回 "写入成功"。
而这个示例也并非没有意义。
比如我们自己想写一个 Agent,我们完全可以使用这种「Thought、Action、Observation」的格式。
以一个开源的模型举个例子(他视频中用的是 Gemma3,而我用的是本地的之前下载的 qwen:4b):

然后把整个内容作为模型输入,直接给模型:
❗注意:System Prompt 与用户问题是要分开给模型的,这里为了简化演示,才把 System Prompt 和 用户问题合在一起直接给到模型。效果差不多的。
bash
ollama run qwen3:4b

之后,按照正常流程,我们应该有段代码去解析 Action 中的内容,将里面的工具和参数提取出来,然后调用这个工具,就像 Cline 接到模型返回的 <use_mcp_tool>...</use_mcp_tool> 的时候那样做。
不过现在是人工测试,并没有什么代码,所以我们这里直接假设调用工具的事情已经做完了,并且拿到了工具的结果。我们把工具结果的前面加上 Observation 一起返回给模型:

模型拿到工具结果之后再次返回了 Thought 和 Action(他视频中是有 Thought 的,我这里也算是有 Thought吧,毕竟有 Thinking),要求使用 write_to_file 工具写入文件。
照例我们假设写入已经完成了,返回给模型 "写入完成" 的消息,最后模型还是先思考一下,发现任务已经完成之后就输出了 FinalAnswer,整个流程就结束了。

从这个流程可以看出,「Thought、Action、Observation」 这个格式可以用来编写 Agent,只是要注意,并不建议使用这种格式,因为这种格式远远没有 XML 那种精确。
当然,我们前面所演示的那个交互流程也可以用 XML 来复现,我们用的 qwen3 模型,大小是 4B 的,难以产出符合格式的 XML。
所以用 DeepSeek 来举例,先用之前演示过的 Client 的 System Prompt,最后加上用户的问题:"纽约明天的天气怎么样?结果写入到 results.md文件中。"。
然后我们把整个文件的内容复制粘贴给 DeepSeek。

DeepSeek 返回了两部分的内容,一个是 Thinking,一个是 use mcp_tool,跟我们在 Cline 那里见到的一模一样。
如果是 Cline 的话,它就会去调用 get_forecast 这个工具了,因为我们在模拟运行,所以我们直接假设工具点完成了返回结果给模型,告诉他纽约明天的天气。模型接到工具调用结果之后,返回了 thinking 和 write about 两个标签,要求写入文件。

同样我们模拟写入成功的消息。模型最后返回了 <attempt_completion>,整个流程就结束了。注意,<attempt_completion> 之前,应该还有个 <thinking>,DeepSeek 失误了,没有把那一部分返回给我们,这点你知道就好了,所以大家也都看到了XML呢,与我们之前所讲的那种 Thought、Action、Observation本质上都是一样的,都遵循 ReAct 的模式,而且模型都能理解。
只是从逻辑上来讲,XML 的效果更好,因为它的表达更为精确。但这也不代表大家就一定要使用 XML 来返回数据,实际上呢,你可以使用任何格式,比如 Json 就是一个很好的选择,只要这个格式本质上用的是 ReAct 的模式,那都是可以的。
所以做一个类似 Cliene 这样的 Agent 需要什么:
首先,我们需要告诉模型返回结果的格式,是用我们前面提到的 Thought、Action、Observation 这样的格式,还是用 Cline 这样的 XML 格式,还是说使用 Json呢?哪一个都行,但一定要明确告知模型。
其次,我们还需要告诉模型可用的工具列表。
最后,我们还需要告诉模型一定要使用 ReAct 模式。也就是说,每次回答的时候,都要先思考一下,思考之后,再接上一个工具调用请求或者是最终答案,我们把这些要求写入到 System Prompt 里面,再加上一个用户问题作为触发,整个 Agent 就可以跑起来了。

Agent的概念、原理与构建
Agent 的概念、原理与构建模式 ------ 从零打造一个简化版的 Claude Code_哔哩哔哩_bilibili
理解 Agent 的概念,原理以及动手构建一个简单的 Agent。
什么是 Agent?
大模型(比如 DeepSeek、ChatGPT)擅长回答问题,但是无法感知或者改变外部环境。
如何解决这个问题?接上对应的工具就行了。
工具就像是大模型的感官和四肢。

像这样,把一个大模型和一堆工具组装起来,变成一个能感知和改变外界环境的智能程序,就称为 Agent。
在图中,Agent 用一个机器人来表示,这与大模型的大脑图标形成了鲜明的对比。

Agent 有很多类型,比如开发程序的、制作 PPT 的、用于深度搜索的等等。总的来说,Agent 的类型很多,擅长的领域也各不相同。
比如,大名鼎鼎的 Cursor 就是一个用于编程的 Agent。
再举个例子,比较火的 Manus,可以让他整理对比几个手机的性能、照相等能力。
ReAct模式的运行流程
Agent 的运行有很多种模式,其中最有名的一种是 ReAct模式 。ReAct 本身是一个缩写,它的全称是 Reasoning and Acting(思考与行动)。
ReAct 可能是目前使用最为广泛的 Agent 运行模式。若要学习 Agent 的实现原理,那就绝对绕不开 ReAct。
这个模式最初由 2022 年 12 月的一篇论文提出,虽然已经过去这么久了,但是它提出的 Agent 的运行模式仍有非常广泛的使用。
在这种模式下的步骤:

可以看出 ReAct 流程的核心步骤是:
Thought、Action、Observation、Final Answer。(记住这几个词,后面会用到)
ReAct模式的实现原理
了解了 ReAct 模式的流程后,下一个问题就是「这种 ReAct 模式是如何实现的?」
为什么模型拿到用户问题后会先思考,再行动,为什么不直接行动?
是因为模型就是这样训练的吗?不是的。这跟模型的训练过程关系不大,大部分奥秘其实都集中在系统提示词上。
系统提示词,是跟用户问题一起送给模型的提示词,它规定了模型的角色、运行时要遵守的规则以及各种环境的信息等等。
比如上节的他讲 MCP 番外篇,那里的系统提示词:

下面演示下如何使用系统提示词(用 DeepSeek 举例):
❗按照规范的做法,系统提示词和用户任务应该分开传给模型。但是 DeepSeek 并未提供单独提交系统提示词的地方,所以我们就把系统提示词和用户任务合在一起,当成一条消息提交给它。
这样的处理方式在大多数的情况下也是没有问题的。模型依然能够按照预期运行。
注意:是大模型请求调用工具,大模型本身是不能调用工具的,调用工具的是 Agent 的工具调用组件。
这里大模型只能是请求。
我们这里是模拟,就假设调用完成了,并且假设工具的返回结果是**"写入成功"**。

以上就是 ReAct 模式真正运行时的节奏:Thought → Action → Observation。一直反复,直到任务完成。最后输出 Thought 和 Final Answer。
系统提示词就相当于给模型安装了一个迷你剧本,它会严格按照这个剧本一步一步走完。
动手实现一个ReAct Agent
前面用 DeepSeek 演示了一个 Agent 的运行流程,可以看到整个流程的关键在于系统提示词。它决定了模型改如何一步步运行。
其实在这个系统提示词的基础上,加上一些配套的代码,我们就可以搭建出一个真正可用的 ReAct Agent。
他已经写好了,直接用他的。下面直接演示下 Agent 的使用过程,随后再一起看一遍代码。

(他用的是 OpenRouter 的 API Key,我用的是 DeepSeek 的 API Key。)
BASH
PS C:\Users\HWL\Desktop\kjisdjidj\Agent_Project> uv run python agent.py snake
请输入任务:写一个贪吃蛇游戏,使用 HTML、CSS 和 js 实现,代码分别放在不同的文件中
正在请求模型,请稍等...
💭 Thought: 我注意到有一个名为 "snake" 的目录,可能这是允许写入的目录。让我尝试将文件写入 snake 目录中。
🔧 Action: write_to_file(snake\index.html, <!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>贪吃蛇游戏</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas" width="400" height="400"></canvas>
<div id="score-board">
<span>分数: </span><span id="score">0</span>
</div>
<button id="restart-btn">重新开始</button>
</div>
<script src="script.js"></script>
</body>
</html>)
🔍 Observation:写入成功
正在请求模型,请稍等...
💭 Thought: 好的,现在写入 CSS 样式文件到 snake 目录中。
🔧 Action: write_to_file(snake\style.css, * {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #1a1a2e;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
#game-container {
background-color: #16213e;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
text-align: center;
}
#gameCanvas {
border: 2px solid #0f3460;
background-color: #0a0a1a;
display: block;
margin: 0 auto;
}
#score-board {
color: #e94560;
font-size: 24px;
font-weight: bold;
margin: 15px 0;
}
#score {
color: #fff;
}
#restart-btn {
background-color: #e94560;
color: white;
border: none;
padding: 10px 30px;
font-size: 18px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
#restart-btn:hover {
background-color: #c73650;
})
🔍 Observation:写入成功
正在请求模型,请稍等...
💭 Thought: 现在写入 JavaScript 文件,实现完整的贪吃蛇游戏逻辑。
🔧 Action: write_to_file(snake\script.js, // 获取 canvas 元素和上下文
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreSpan = document.getElementById('score');
const restartBtn = document.getElementById('restart-btn');
// 游戏配置
const GRID_SIZE = 20; // 网格大小(像素)
const GRID_COUNT = 20; // 网格数量 (20x20)
const TICK_INTERVAL = 150; // 游戏速度(毫秒)
// 游戏状态变量
let snake = []; // 蛇身坐标数组
let food = {}; // 食物坐标
let direction = 'RIGHT'; // 当前移动方向
let nextDirection = 'RIGHT'; // 下一帧方向(用于缓冲)
let score = 0;
let gameRunning = false;
let gameInterval = null;
// 初始化游戏
function initGame() {
// 蛇的初始位置:中间偏左,初始长度为3
snake = [
{ x: 7, y: 10 },
{ x: 6, y: 10 },
{ x: 5, y: 10 }
];
direction = 'RIGHT';
nextDirection = 'RIGHT';
score = 0;
scoreSpan.textContent = '0';
generateFood();
gameRunning = true;
// 清除之前的定时器
if (gameInterval) {
clearInterval(gameInterval);
}
gameInterval = setInterval(gameTick, TICK_INTERVAL);
}
// 生成食物(确保不在蛇身上)
function generateFood() {
let newFood;
do {
newFood = {
x: Math.floor(Math.random() * GRID_COUNT),
y: Math.floor(Math.random() * GRID_COUNT)
};
} while (snake.some(segment => segment.x === newFood.x && segment.y === newFood.y));
food = newFood;
}
// 游戏主循环(每一帧)
function gameTick() {
if (!gameRunning) return;
// 更新方向
direction = nextDirection;
// 计算新的蛇头位置
const head = snake[0];
let newHead = { ...head };
switch (direction) {
case 'UP':
newHead.y--;
break;
case 'DOWN':
newHead.y++;
break;
case 'LEFT':
newHead.x--;
break;
case 'RIGHT':
newHead.x++;
break;
}
// 检查是否吃到食物
const ateFood = (newHead.x === food.x && newHead.y === food.y);
// 构建新蛇
let newSnake = [newHead, ...snake];
if (!ateFood) {
// 没吃到食物,移除尾部
newSnake.pop();
}
// 碰撞检测:撞墙或撞自身
const headCollision =
newHead.x < 0 || newHead.x >= GRID_COUNT ||
newHead.y < 0 || newHead.y >= GRID_COUNT ||
newSnake.slice(1).some(segment => segment.x === newHead.x && segment.y === newHead.y);
if (headCollision) {
gameRunning = false;
clearInterval(gameInterval);
gameInterval = null;
drawGame();
return;
}
// 更新蛇
snake = newSnake;
if (ateFood) {
score++;
scoreSpan.textContent = score;
generateFood();
}
// 绘制游戏画面
drawGame();
}
// 绘制游戏
function drawGame() {
// 清空画布
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制网格线(浅色辅助线)
ctx.strokeStyle = '#1a1a2e';
ctx.lineWidth = 0.5;
for (let i = 0; i <= GRID_COUNT; i++) {
ctx.beginPath();
ctx.moveTo(i * GRID_SIZE, 0);
ctx.lineTo(i * GRID_SIZE, canvas.height);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i * GRID_SIZE);
ctx.lineTo(canvas.width, i * GRID_SIZE);
ctx.stroke();
}
// 绘制食物
ctx.fillStyle = '#e94560';
ctx.shadowColor = '#e94560';
ctx.shadowBlur = 10;
ctx.beginPath();
const fx = food.x * GRID_SIZE + GRID_SIZE / 2;
const fy = food.y * GRID_SIZE + GRID_SIZE / 2;
ctx.arc(fx, fy, GRID_SIZE / 2 - 2, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
// 绘制蛇身
snake.forEach((segment, index) => {
const x = segment.x * GRID_SIZE;
const y = segment.y * GRID_SIZE;
const padding = 1;
const radius = 4;
if (index === 0) {
// 蛇头:亮绿色
ctx.fillStyle = '#4ecca3';
ctx.shadowColor = '#4ecca3';
ctx.shadowBlur = 8;
} else {
// 蛇身:渐变的绿色
const intensity = 0.6 - (index / snake.length) * 0.3;
ctx.fillStyle = `rgb(46, 160, ${Math.floor(120 + intensity * 60)})`;
ctx.shadowBlur = 0;
}
ctx.beginPath();
ctx.roundRect(x + padding, y + padding, GRID_SIZE - padding * 2, GRID_SIZE - padding * 2, radius);
ctx.fill();
});
ctx.shadowBlur = 0;
// 如果游戏结束,显示提示文字
if (!gameRunning) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#e94560';
ctx.font = 'bold 36px Segoe UI, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('游戏结束', canvas.width / 2, canvas.height / 2);
}
}
// 辅助方法:画圆角矩形
CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) {
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
this.moveTo(x + r, y);
this.lineTo(x + w - r, y);
this.quadraticCurveTo(x + w, y, x + w, y + r);
this.lineTo(x + w, y + h - r);
this.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
this.lineTo(x + r, y + h);
this.quadraticCurveTo(x, y + h, x, y + h - r);
this.lineTo(x, y + r);
this.quadraticCurveTo(x, y, x + r, y);
this.closePath();
return this;
};
// 键盘控制
document.addEventListener('keydown', function (event) {
if (!gameRunning) return;
const key = event.key;
// 阻止箭头键滚动页面
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(key)) {
event.preventDefault();
}
// 方向映射,不允许掉头
switch (key) {
case 'ArrowUp':
if (direction !== 'DOWN') nextDirection = 'UP';
break;
case 'ArrowDown':
if (direction !== 'UP') nextDirection = 'DOWN';
break;
case 'ArrowLeft':
if (direction !== 'RIGHT') nextDirection = 'LEFT';
break;
case 'ArrowRight':
if (direction !== 'LEFT') nextDirection = 'RIGHT';
break;
}
});
// 重新开始按钮
restartBtn.addEventListener('click', function () {
initGame();
});
// 开始游戏
initGame();
)
🔍 Observation:写入成功
正在请求模型,请稍等...
💭 Thought: 所有文件已成功写入 snake 目录。让我验证一下文件是否都正确创建。
🔧 Action: run_terminal_command(dir snake)
是否继续?(Y/N)Y
🔍 Observation:驱动器 C 中的卷是 OS
卷的序列号是 28F3-60CA
C:\Users\HWL\Desktop\kjisdjidj\Agent_Project\snake 的目录
2026/06/04 22:47 <DIR> .
2026/06/04 22:46 <DIR> ..
2026/06/04 22:47 579 index.html
2026/06/04 22:47 6,741 script.js
2026/06/04 22:47 1,005 style.css
3 个文件 8,325 字节
2 个目录 360,887,078,912 可用字节
正在请求模型,请稍等...
✅ Final Answer:贪吃蛇游戏已创建完成,所有文件放在 `snake` 目录下,包含三个文件:
1. **index.html** - HTML 结构文件,包含 canvas 画布、分数显示和重新开始按钮,并引用了 CSS 和 JS 文件。
2. **style.css** - 样式文件,采用深色主题风格,包含游戏容器、画布、分数板和按钮的美化样式。
3. **script.js** - JavaScript 游戏逻辑文件,包含完整的贪吃蛇功能:
- 20x20 网格,蛇初始长度为3
- 键盘方向键控制(防止掉头)
- 随机生成食物(不会出现在蛇身上)
- 碰墙或撞自身游戏结束
- 分数实时更新
- 重新开始按钮
- 圆角蛇身和发光食物视觉效果
- 游戏结束提示画面
打开 `snake` 文件夹中的 `index.html` 即可开始玩贪吃蛇游戏。
PS C:\Users\HWL\Desktop\kjisdjidj\Agent_Project>

打开,是可以玩的。
下面来看下这个 Agent 的具体代码:
python
import ast
import inspect
import os
import re
from string import Template
from typing import List, Callable, Tuple
import click
from dotenv import load_dotenv
from openai import OpenAI
import platform
from prompt_template import react_system_prompt_template
class ReActAgent:
def __init__(self, tools: List[Callable], model: str, project_directory: str):
self.tools = { func.__name__: func for func in tools }
self.model = model
self.project_directory = project_directory
self.client = OpenAI(
base_url="https://api.deepseek.com",
api_key=ReActAgent.get_api_key(),
)
def run(self, user_input: str):
messages = [
{"role": "system", "content": self.render_system_prompt(react_system_prompt_template)},
{"role": "user", "content": f"<question>{user_input}</question>"}
]
while True:
# 请求模型
content = self.call_model(messages)
# 检测 Thought
thought_match = re.search(r"<thought>(.*?)</thought>", content, re.DOTALL)
if thought_match:
thought = thought_match.group(1)
print(f"\n\n💭 Thought: {thought}")
# 检测模型是否输出 Final Answer,如果是的话,直接返回
if "<final_answer>" in content:
final_answer = re.search(r"<final_answer>(.*?)</final_answer>", content, re.DOTALL)
return final_answer.group(1)
# 检测 Action
action_match = re.search(r"<action>(.*?)</action>", content, re.DOTALL)
if not action_match:
raise RuntimeError("模型未输出 <action>")
action = action_match.group(1)
tool_name, args = self.parse_action(action)
print(f"\n\n🔧 Action: {tool_name}({', '.join(args)})")
# 只有终端命令才需要询问用户,其他的工具直接执行
should_continue = input(f"\n\n是否继续?(Y/N)") if tool_name == "run_terminal_command" else "y"
if should_continue.lower() != 'y':
print("\n\n操作已取消。")
return "操作被用户取消"
try:
observation = self.tools[tool_name](*args)
except Exception as e:
observation = f"工具执行错误:{str(e)}"
print(f"\n\n🔍 Observation:{observation}")
obs_msg = f"<observation>{observation}</observation>"
messages.append({"role": "user", "content": obs_msg})
def get_tool_list(self) -> str:
"""生成工具列表字符串,包含函数签名和简要说明"""
tool_descriptions = []
for func in self.tools.values():
name = func.__name__
signature = str(inspect.signature(func))
doc = inspect.getdoc(func)
tool_descriptions.append(f"- {name}{signature}: {doc}")
return "\n".join(tool_descriptions)
def render_system_prompt(self, system_prompt_template: str) -> str:
"""渲染系统提示模板,替换变量"""
tool_list = self.get_tool_list()
file_list = ", ".join(
os.path.abspath(os.path.join(self.project_directory, f))
for f in os.listdir(self.project_directory)
)
return Template(system_prompt_template).substitute(
operating_system=self.get_operating_system_name(),
tool_list=tool_list,
file_list=file_list
)
@staticmethod
def get_api_key() -> str:
"""Load the API key from an environment variable."""
load_dotenv()
api_key = os.getenv("DEEPSEEK_API_KEY")
if not api_key:
raise ValueError("未找到 DEEPSEEK_API_KEY 环境变量")
return api_key
def call_model(self, messages):
print("\n\n正在请求模型,请稍等...")
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
)
content = response.choices[0].message.content
messages.append({"role": "assistant", "content": content})
return content
def parse_action(self, code_str: str) -> Tuple[str, List[str]]:
match = re.match(r'(\w+)\((.*)\)', code_str, re.DOTALL)
if not match:
raise ValueError("Invalid function call syntax")
func_name = match.group(1)
args_str = match.group(2).strip()
# 手动解析参数,特别处理包含多行内容的字符串
args = []
current_arg = ""
in_string = False
string_char = None
i = 0
paren_depth = 0
while i < len(args_str):
char = args_str[i]
if not in_string:
if char in ['"', "'"]:
in_string = True
string_char = char
current_arg += char
elif char == '(':
paren_depth += 1
current_arg += char
elif char == ')':
paren_depth -= 1
current_arg += char
elif char == ',' and paren_depth == 0:
# 遇到顶层逗号,结束当前参数
args.append(self._parse_single_arg(current_arg.strip()))
current_arg = ""
else:
current_arg += char
else:
current_arg += char
if char == string_char and (i == 0 or args_str[i-1] != '\\'):
in_string = False
string_char = None
i += 1
# 添加最后一个参数
if current_arg.strip():
args.append(self._parse_single_arg(current_arg.strip()))
return func_name, args
def _parse_single_arg(self, arg_str: str):
"""解析单个参数"""
arg_str = arg_str.strip()
# 如果是字符串字面量
if (arg_str.startswith('"') and arg_str.endswith('"')) or \
(arg_str.startswith("'") and arg_str.endswith("'")):
# 移除外层引号并处理转义字符
inner_str = arg_str[1:-1]
# 处理常见的转义字符(\\ 必须最先替换,否则会错误消费其他转义序列)
inner_str = inner_str.replace('\\\\', '\\')
inner_str = inner_str.replace('\\"', '"').replace("\\'", "'")
inner_str = inner_str.replace('\\n', '\n').replace('\\t', '\t')
inner_str = inner_str.replace('\\r', '\r')
return inner_str
# 尝试使用 ast.literal_eval 解析其他类型
try:
return ast.literal_eval(arg_str)
except (SyntaxError, ValueError):
# 如果解析失败,返回原始字符串
return arg_str
def get_operating_system_name(self):
os_map = {
"Darwin": "macOS",
"Windows": "Windows",
"Linux": "Linux"
}
return os_map.get(platform.system(), "Unknown")
_project_dir = None
def _resolve_path(file_path):
"""将路径限制在项目目录内,防止越权访问"""
abs_path = os.path.abspath(file_path)
if not abs_path.startswith(os.path.abspath(_project_dir)):
raise ValueError(f"禁止访问项目目录外的文件: {file_path}")
return abs_path
def read_file(file_path):
"""用于读取文件内容"""
with open(_resolve_path(file_path), "r", encoding="utf-8") as f:
return f.read()
def write_to_file(file_path, content):
"""将指定内容写入指定文件"""
with open(_resolve_path(file_path), "w", encoding="utf-8") as f:
f.write(content)
return "写入成功"
def run_terminal_command(command):
"""用于执行终端命令"""
import subprocess
import platform
if platform.system() == "Windows":
command = f'cmd /c "{command}"'
run_result = subprocess.run(command, shell=True, capture_output=True, text=True)
if run_result.returncode == 0:
return run_result.stdout.strip() if run_result.stdout.strip() else "执行成功"
else:
return f"执行失败: {run_result.stderr.strip()}" if run_result.stderr.strip() else f"执行失败,退出码: {run_result.returncode}"
@click.command()
@click.argument('project_directory',
type=click.Path(exists=True, file_okay=False, dir_okay=True))
def main(project_directory):
global _project_dir
project_dir = os.path.abspath(project_directory)
_project_dir = project_dir
tools = [read_file, write_to_file, run_terminal_command]
# 这个 ReActAgent 就是核心了
agent = ReActAgent(tools=tools, model="deepseek-v4-flash", project_directory=project_dir)
task = input("请输入任务:")
final_answer = agent.run(task) # 这个函数是 ReActAgent 的核心
print(f"\n\n✅ Final Answer:{final_answer}")
if __name__ == "__main__":
main()
ReAct 运行时序图
为了彻底明白这其中发生了什么,我们来画个 Agent 的流程图。
整个流程图里面有 2 个角色:用户 和 Agent。
而 Agent 又可以分为 3 个部分:
- 模型
- 工具(函数)
- Agent 主程序 (就是 Agent 里负责串联整个流程的代码逻辑,会在合适的时候调用工具或模型等,可以大致理解为刚才代码里的
run函数)。
完整的 ReAct Agent 的问答流程图:

Plan-And-Execute模式介绍
前面讲了下通过 ReAct 模式来构建 Agent,ReAct 是目前最常见,使用最广泛的 Agent 构建模式,但它不是唯一的方案。除了 ReAct 之外,还有很多其他的运行模式。
其中很多 Agent 的运行过程:先规划,再执行。
比如之前的 Manus,它在回答的时候,会先构建一个待办列表。后面的执行过程,就是遵循这个待办列表来的。

而 ClaudeCode 中,也会经常看到这种先创建 TODO,再去执行的情况。

这种「先规划,再执行」的情况,目前并没有一个统一的名字。而且每个 Agent 的实现多多少少也有一些差别。
这其中一个比较有名的实现,是 LangChain 提出来的 Plan-and-Execute 模式。
从总体上来看,它也是遵循了「先规划,再执行」的流程,只是它的流程引入了一些动态修改规划的环节。这使得它的方案有了很大的灵活性。
Plan-And-Execute运行流程

Plan-and-Execute 的具体实现代码:
可到 LangChain 官方获取:LangChain overview - Docs by LangChain

LangGraph+MCP多智能体开发实战
B站唯一讲得最好的LangGraph+MCP智能体开发实战教程,手把手教你使用LangGraph构建多智能体工作流,少走99%弯路!_哔哩哔哩_bilibili
学习 LangGraph 框架,并且使用 LangGraph 框架接入MCP服务,完成多智能体工作流搭建。
一、快速了解LangGraph
LangGraph是⼀个功能非常强⼤的⼤语⾔模型本地应⽤构建框架。不只是包含了各种基于⼤语⾔模型构建本地应用的工具,更重要的是,他积累了非常多使用大语⾔模型构建本地应⽤的经验,并且将这些经验总结成了非常多的案例,让⼤家可以直接使用。
但是,LangGraph 并不是⼀个独立的框架,他是 LangChain 框架的⼀个⽣态组件。所以,如果脱离 LangChain 来介绍 LangGraph,那纯粹是耍流氓。
因此后⾯的内容,是预设⼤家有 LangChain 的基础的,并且最好是看过我之前分享的 LangChain 视频。
课程目标:
- 理解 LangGraph 与 LangChain
- 理解 Agent 智能体
- 理解智能体编排
- 如何部署 LangChain 应⽤
LangGraph到底是干什么的?
LangChain ,官网地址:https://python.langchain.com/docs/introduction/ 是⼀个用于开发由大语言模型 LLM 提供支持的应用程序的框架。
简单来说,就是⼀个用 LLM 快速构建本地应⽤的框架。当前最新的版本是 V0.3。后续内容也以这个版本为准。
在 LangChain 中,除了 LangChain 外,还有 LangGraph、LangSmith 等⼀系列的生态组件。

其中,LangChain 是整个生态的基础。
LangSmith 主要是针对 LangChain 的应用进行测试、监控和分析的平台。
LangGraph 则是基于 LangChain 的应⽤程序开发框架,它可以帮助开发者更方便地构建和管理复杂的应⽤程序。
LangGraph的官网地址:https://langchain-ai.github.io/langgraph/ 。
从页面左侧的菜单可以看到,使用 LangGraph 构建应用的标准流程是这样:
-
Prebuilt Agents :第⼀步,构建 Agent 智能体。这是 LangGraph 应用的基础
-
LangGraph Framework :第二步,构建 LangGraph 应用。主要是以 Graph 图的方式将多个 Agent 智能体整合成⼀个整体。这也是 LangGraph 最核心的部分
-
LangGraph Platform:第三步,通过 LangGraph Platform 平台部署应用。这是一个商业化的平台,可以以标准化的形式部署 LangGraph 应用,并提供测试、监控、分析等功能。
其实,对于 LangGraph 框架,如果你把这几个部分都搞清楚了,那么,整个 LangGraph 框架,你也就通了一大半了。
对于构建应用来说,前两步必不可少。而第三步则通常不是一个必须选项。所以接下来我们重点介绍前两个部分。
这一章先来介绍第⼀步,构建智能体。这对于 LangGraph 来说,是⼀个非常重要的部分。因为 LangGraph 是基于 Agent 的,所以构建 Agent 是 LangGraph 的基础。
这里的 Agent 智能体,其实本质上就是将大语言模型的各种功能,封装成独立的整体。Agent 构建完成后,未来我们有什么问题,直接交给 Agent 处理就行了。不用过多关注 Agent 的细节。而与大模型交互这个事情,LangChain 框架已经实现了非常多的核心功能,所以,这一部分也是和 LangChain 联系非常紧密的⼀个部分。
快速体验LangChain和LangGraph
LangGraph 和 LangChain 都是用于构建和管理大型语言模型应用的工具,它们都提供了一种简单易用的方式来构建和管理复杂的应用程序。
只不过,LangChain 更关注于应用程序的整体流程,而 LangGraph 更关注于如何处理特定的任务。
关于 LangGraph 的细节,在后续的章节中详细介绍。这里,先用最简单直白的方式来对比下 LangChain 和 LangGraph 在大语言模型做交互时的基础思想有什么区别。
要用 LangGraph,首先需要安装 LangGraph 的依赖库:
按照 ChatGPT 的教程。
在终端安装依赖:
bash
pip install langgraph langchain langchain-community
实际上,从依赖库的安装过程就能看到,LangGraph 是依赖于 LangChain 库的。
然后,先从基础的访问大模型的 API 开始,比较下 LangChain 和 LangGraph 访问大模型的 API 的区别:
使用 LangChain 访问大模型最基础的方式是使用 init_chat_model 创建一个ChatModel,大模型对象。通过这个大模型对象去完成与大模型的交互。

python
"""
LangGraph 快速入门示例 - 对应 PDF 中的代码
"""
import datetime
import os
from langchain.tools import tool
from langchain_community.chat_models import ChatTongyi
from langgraph.prebuilt import create_react_agent
# =============================================
# 第一步:配置 API Key
# =============================================
# 方式一:阿里云百炼(国内可用,无需 VPN)
# 去 https://bailian.console.aliyun.com/ 获取 API Key
# 从系统环境变量读取(已配置在系统环境变量中的话,这行可以注释掉或删掉)
# os.environ["DASHSCOPE_API_KEY"] = "你的百炼API_KEY"
# 方式二:OpenAI(需要网络环境)
# os.environ["OPENAI_API_KEY"] = "你的OPENAI_API_KEY"
# =============================================
# 第二步:初始化 LLM
# =============================================
# 使用通义千问(国内推荐)
llm = ChatTongyi(
model="qwen-plus", # 可选: qwen-plus, qwen-max, qwen-turbo
)
# 如果用 OpenAI:
# from langchain.chat_models import init_chat_model
# llm = init_chat_model("gpt-4o-mini", model_provider="openai")
# 先测试模型能否正常工作
print("=" * 50)
print("测试模型调用...")
response = llm.invoke("你好,请说一句话")
print(f"模型回复: {response.content}")
print("=" * 50)
# =============================================
# 第三步:定义工具(Tool) 注意要添加注释
# =============================================
@tool
def get_current_date():
"""获取今天的日期"""
return datetime.datetime.today().strftime("%Y-%m-%d")
@tool
def get_weather(city: str):
"""查询指定城市的天气(模拟)"""
weather_data = {
"北京": "晴天,25°C",
"上海": "多云,28°C",
"深圳": "雷阵雨,30°C",
}
return weather_data.get(city, f"暂无{city}的天气数据")
# =============================================
# 第四步:传统 LangChain 工具调用方式(手动循环)
# =============================================
print("\n>>> 方式一:传统 LangChain 工具调用")
# 大模型绑定工具
llm_with_tools = llm.bind_tools([get_current_date, get_weather])
# 工具容器
all_tools = {"get_current_date": get_current_date, "get_weather": get_weather}
# 把所有消息存到一起
messages = [{"role": "user", "content": "今天是什么日期?"}]
# 询问大模型。大模型会判断是否需要调用工具,并返回一个工具调用请求
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
if hasattr(ai_msg, "tool_calls") and ai_msg.tool_calls:
for tool_call in ai_msg.tool_calls:
selected_tool = all_tools[tool_call["name"].lower()]
print(f" 调用工具: {tool_call['name']}, 参数: {tool_call['args']}")
tool_msg = selected_tool.invoke(tool_call)
messages.append(tool_msg)
final_result = llm_with_tools.invoke(messages)
print(f" 最终回复: {final_result.content}")
# =============================================
# 第五步:LangGraph Agent 方式(自动循环)
# =============================================
print("\n>>> 方式二:LangGraph Agent(推荐)")
agent = create_react_agent(
model=llm,
tools=[get_current_date, get_weather],
prompt="你是一个有帮助的助手。请使用工具来回答用户的问题。",
)
result = agent.invoke({
"messages": [{"role": "user", "content": "今天是什么日期?北京天气怎么样?"}]
})
# 输出完整对话
for msg in result["messages"]:
role = msg.type
if role == "ai":
if hasattr(msg, "tool_calls") and msg.tool_calls:
for tc in msg.tool_calls:
print(f" [Agent 调用工具] {tc['name']}({tc['args']})")
else:
print(f" [Agent 回复] {msg.content}")
elif role == "tool":
print(f" [工具返回] {msg.content}")
elif role == "human":
print(f" [用户提问] {msg.content}")
print("\n✅ 完成!LangGraph 自动处理了工具调用循环。")

小结
从这个案例大概可以简单的感受到,LangGraph 的一部分核心功能就是要在LangChain 的基础上,以 Agent 智能体的方式,提供更简单实用的功能封装,从而让我们可以简单方便地使用 LangChain 的功能。
当然,Agent 的功能封装远不止这个案例中这么简单。通过 LangGraph 的 Agent 功能,可以将与大模型交互的各种基础功能统一封装成独立的 Agent,而不用过多关注 Agent 内部的实现细节。
接下来,有了 Agent 之后,LangGraph 还通过 Graph 图的方式,可以将多个 Agent 智能体串联起来,实现更加复杂的应用。
例如,有了一个查询今天日期的 Agent,接下来可以再实现查询航班信息的 Agent,将这两个 Agent 串联起来,就可以完成一个出行规划的复合任务。
这些会在后面章节逐步介绍。
二、使用LangGraph构建Agent智能体
这一节,我们使用 LangGraph 来重新构建一个专属的聊天机器人。并以此为案例,逐步构建一个功能强大、又安全可控的聊天 Agent 智能体。再以此为基础,为后续 LangGraph 构建多智能体提供基础支撑。
- 使用 LangGraph 构建 Agent 智能体
- Agent 智能体增加 Tools 工具调用机制
- Agent 智能体消息记忆管理功能
- Human-In-Loop 人类监督功能
LangGraph 是和 LangChain 是一体的。他说,任何抛开 LangChain 独立讲解 LangGraph 的,都是耍流氓。所以,后续内容设计都是在 LangChain 的基础上构建,并且最好是能够跟上他之前分享 LangChain 的思路。
什么是 Agent?
Agent 智能体,是 LangGraph 中的一个核心概念。很多朋友也应该经常在网上听说 Agent 智能体,但是,到底什么是智能体呢?
这个问题没有标准答案。而 LangGraph 给我们勾勒出了一个好的 Agent 智能体的形象。这个 Agent,可以类比于一个好的员工。什么是好的员工呢?自然是希望这个员工既能力强大,可以放心地独立完成任务,而不用过多干预实现的细节。同时,这个员工还要能够听从安排,关键节点要及时跟领导请示,并及时地根据领导的指示调整自己的工作进度。
具体到 LangGraph 的实现中,Agent 既需要拥有封装与大模型交互的所有基础能力,包括访问大模型、调用 Tools、保存 ChatMemory 等这些基础的能力,可以独立完成一系列基于大模型构建的任务;同时,又可以随时干预 Agent 执行进度,对关键步骤做出调整。
构建聊天 Agent 智能体
如何构建这样强大并且听话的 Agent 智能体呢?我们先从基础的大模型聊天开始。
在 LangChain 中,提供了 ChatModels 接口,用于访问大模型。我们可以通过调用 ChatModels 的接口来访问大模型。
python
# 尝试多种导入方式
try:
from langchain_alibaba.chat_models import ChatTongyi
except ImportError:
try:
from langchain_alibaba import ChatTongyi
except ImportError:
try:
from langchain_community.chat_models import ChatTongyi
import warnings
warnings.filterwarnings('ignore', category=DeprecationWarning)
except ImportError:
raise ImportError("无法导入 ChatTongyi,请检查包安装")
from config.load_key import load_key
# 构建阿里云百炼大模型客户端
llm = ChatTongyi(
model="qwen-plus",
api_key=load_key("DASHSCOPE_API_KEY"),
)
# 直接打印完整返回对象
result = llm.invoke("你是谁?能帮我解决什么问题?")
print(result)

json
content='你好!我是通义千问(Qwen),阿里巴巴集团旗下的超大规模语言模型。我能够理解并生成多种语言的文本,擅长回答问题、创作文字(如写故事、公文、邮件、剧本等)、逻辑推理、编程、多语言支持,还能进行观点表达、角色扮演,甚至帮你分析和总结文档、表格、论文等内容。\n\n我能帮你的事情包括但不限于:\n\n✅ **学习与教育**:解释知识点、解数学/物理题、梳理历史脉络、辅助英语学习、撰写读书报告或论文提纲 \n✅ **工作与办公**:起草会议纪要、撰写简历/求职信、优化PPT文案、编写邮件、制定项目计划 \n✅ **创意写作**:写诗歌、小说、广告语、短视频脚本、节日祝福、脱口秀段子 \n✅ **编程开发**:解释代码逻辑、调试建议、生成Python/Java/SQL等代码片段、写正则表达式、设计算法思路 \n✅ **生活助手**:提供健康小贴士、旅行建议、食谱推荐、亲子沟通话术、情绪疏导小练习 \n✅ **实用工具**:中英等多语种翻译、文本润色、摘要提炼、逻辑检查、信息查证(基于我训练截止前的知识)\n\n⚠️ 温馨提示:我的知识截止于2024年10月,无法实时联网获取最新资讯(如今日股价、突发新闻),也不具备个人情感或真实身份。但我始终尽力提供准确、有用、友善且符合中国法规与价值观的帮助。\n\n如果你有任何具体问题、任务或想法------比如"帮我把这段技术文档翻译成英文""想为孩子设计一个科学小实验""用Python画一个动态心形图""分析这段合同条款的风险点"......欢迎随时告诉我,我立刻为你效劳!😊' additional_kwargs={} response_metadata={'model_name': 'qwen-plus', 'finish_reason': 'stop', 'request_id': 'f0ebba7b-dcc6-9150-907b-f2c149287e5c', 'token_usage': {'input_tokens': 17, 'output_tokens': 390, 'total_tokens': 407, 'prompt_tokens_details': {'cached_tokens': 0}}} id='lc_run--019ea179-be37-7a90-9998-962af47f953e-0' tool_calls=[] invalid_tool_calls=[]
而 LangGraph 只要将这个 ChatModel 进行简单的封装,就可以完成与大模型的交互。
python
from langchain_community.chat_models import ChatTongyi
from langgraph.prebuilt import create_react_agent
from config.load_key import load_key
import warnings
warnings.filterwarnings('ignore')
llm = ChatTongyi(
model="qwen-plus",
api_key=load_key("DASHSCOPE_API_KEY"),
)
agent = create_react_agent(
model=llm,
tools=[],
prompt="You are a helpful assistant",
)
result = agent.invoke({"messages": [{"role": "user", "content": "你是谁?能帮我解决什么问题?"}]})
print(result)

json
{'messages': [HumanMessage(content='你是谁?能帮我解决什么问题?', additional_kwargs={}, response_metadata={}, id='f4d1d00c-7c1c-4472-bc71-309837fab955'), AIMessage(content='你好!我是通义千问(Qwen),阿里巴巴集团旗下的超大规模语言模型。我能够理解并生成多种语言,擅长回答问题、创作文字(如写故事、公文、邮件、剧本等)、逻辑推理、编程、多语言支持,还能进行知识问答、学习辅导、观点表达、玩游戏等。\n\n你可以让我帮你:\n✅ 解答学习或生活中的各种问题(数学、物理、历史、语言等) \n✅ 撰写和润色文章、简历、演讲稿、工作总结、创意文案等 \n✅ 编程辅助(Python/Java/JavaScript等代码编写、调试、解释) \n✅ 翻译与跨语言交流(支持上百种语言) \n✅ 制定计划(学习计划、旅行规划、健身饮食建议等) \n✅ 陪伴聊天、头脑风暴、角色扮演、甚至一起写小说或设计谜题 😄 \n\n只要你有需求,尽管告诉我------清晰描述你的目标或问题,我会尽力提供准确、有用、有温度的帮助!\n\n你现在有什么想解决的问题吗?😊', additional_kwargs={}, response_metadata={'model_name': 'qwen-plus', 'finish_reason': 'stop', 'request_id': '03af62cf-2eff-92fe-8af7-0df3b2f85a47', 'token_usage': {'input_tokens': 27, 'output_tokens': 223, 'total_tokens': 250, 'prompt_tokens_details': {'cached_tokens': 0}}}, id='lc_run--019ea19d-4972-7863-b24d-3da39c25b30f-0', tool_calls=[], invalid_tool_calls=[])]}
当然,Agent 也支持常用的 Stream 流式输出的方式。
python
for chunk in agent.stream(
{"messages":[{"role": "user", "content": "你是谁?能帮我解决什么问题?"}]},
stream_mode = "messages"
):
print(chunk)
print("\n")
这里的 stream_mode 有三种选项:
- updates:流式输出每个工具调用的每个步骤
- messages:流失输出大语言模型回复的 Token
- values:一次拿到所有的 chunk,默认值。
- custom:自定义输出。主要是可以在工具内部使用 get_stream_writer 获取输入流,添加自定义的内容。
关于流式输出的这几种选项,在后面结合 Graph,会体现出更大的作用。
增加 Tools 工具调用
Tools 工具机制是大语言模型中的一个重要机制,它可以让大模型调用外部工具,从而实现更加复杂的功能。通常,一个完整的工具调用流程需要以下几个步骤:
- 客户端定义工具类,实现工具的功能;
- 客户端请求大语言模型,带上问题以及工具的描述信息;
- 大语言模型综合判断问题,并决定是否调用工具;
- 如果大语言模型判断需要使用工具,就会向客户端返回一个带有 tool_calls 工具调用信息的 AIMessage;
- 客户端根据工具调用信息,调用工具,并将结果返回给大语言模型;
- 大语言模型根据工具调用的结果,生成最终的回答。
python
import datetime
from langchain.agents import create_agent
from langchain_community.chat_models import ChatTongyi
from config.load_key import load_key
llm = ChatTongyi(
model="qwen-plus",
api_key=load_key("DASHSCOPE_API_KEY"),
)
def get_current_date():
"""获取今天的日期"""
return datetime.datetime.today().strftime("%Y-%m-%d")
agent = create_agent(
model=llm,
tools=[get_current_date],
system_prompt="你是一个智能助手",
)
result = agent.invoke({"messages": [{"role": "user", "content": "今天是几月几号"}]})
print(result)

json
{'messages': [HumanMessage(content='今天是几月几号', additional_kwargs={}, response_metadata={}, id='92ad2267-6959-4c0e-88e7-3a3fe2f1ff0f'), AIMessage(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_1ed1d5b2b0d04924a24231', 'type': 'function', 'function': {'name': 'get_current_date', 'arguments': '{}'}}]}, response_metadata={'model_name': 'qwen-plus', 'finish_reason': 'tool_calls', 'request_id': '93446ffa-9195-9da3-ba40-a0c02186334a', 'token_usage': {'input_tokens': 140, 'output_tokens': 16, 'total_tokens': 156, 'prompt_tokens_details': {'cached_tokens': 0}}}, id='lc_run--019ea242-b83f-74c1-a93e-bcd3c31a90d9-0', tool_calls=[{'name': 'get_current_date', 'args': {}, 'id': 'call_1ed1d5b2b0d04924a24231', 'type': 'tool_call'}], invalid_tool_calls=[]), ToolMessage(content='2026-06-07', name='get_current_date', id='7afad98b-f1a9-45f4-bc4d-b67c51552010', tool_call_id='call_1ed1d5b2b0d04924a24231'), AIMessage(content='今天是2026年6月7日。', additional_kwargs={}, response_metadata={'model_name': 'qwen-plus', 'finish_reason': 'stop', 'request_id': 'e2eecdb1-ce48-9aa0-b05f-a1f4f5b806fd', 'token_usage': {'input_tokens': 180, 'output_tokens': 12, 'total_tokens': 192, 'prompt_tokens_details': {'cached_tokens': 0}}}, id='lc_run--019ea242-bf8b-7013-bc5f-5e5ec37aebbf-0', tool_calls=[], invalid_tool_calls=[])]}
从返回的结果可看到,LangGraph 的 Agent 完整地封装了工具调用的整个流程。而我们所需关注的,只是构建出带有工具信息的 Agent,然后把用户的问题交给 Agent 去处理就行了。
在定义工具时,除了可以从工具函数的注释中获取工具描述信息外,LangGraph 也同样兼容了 LangChain 中使用 @tool 注解声明工具的方式。
如果工具执行时出错了,LangGraph 也提供了主动处理异常信息的能力。
python
from langchain_community.chat_models import ChatTongyi
from langchain_core.messages import SystemMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, MessagesState, START
from langgraph.prebuilt import ToolNode, tools_condition
from config.load_key import load_key
# 定义工具 return_direct=True 表示直接返回工具的结果
@tool("devide_tool", return_direct=True)
def devide(a: int, b: int) -> float:
"""计算两个整数的除法。
Args:
a: 除数
b: 被除数
"""
if b == 1:
raise ValueError("除数不能为1")
return a / b
print("工具名称:", devide.name)
print("工具描述:", devide.description)
print("工具参数:", devide.args)
# 定义工具调用错误处理函数
def handle_tool_error(error: Exception) -> str:
"""处理工具调用错误。"""
if isinstance(error, ValueError):
return "除数为1没有意义,请重新输入一个除数和被除数。"
elif isinstance(error, ZeroDivisionError):
return "除数不能为0,请重新输入一个除数和被除数。"
return f"工具调用错误:{error}"
# 创建 ToolNode,绑定错误处理
tool_node = ToolNode(
[devide],
handle_tool_errors=handle_tool_error,
)
# 初始化 LLM
llm = ChatTongyi(
model="qwen-plus",
api_key=load_key("DASHSCOPE_API_KEY"),
)
system_prompt = "你是一个数学助手。当工具返回错误时,请根据错误信息重新尝试。"
# 定义 LLM 节点
def call_model(state: MessagesState):
messages = state["messages"]
if not any(isinstance(m, SystemMessage) for m in messages):
messages = [SystemMessage(content=system_prompt)] + list(messages)
return {"messages": [llm.invoke(messages)]}
# 手动构建 agent 图
builder = StateGraph(MessagesState)
builder.add_node("chatbot", call_model)
builder.add_node("tools", tool_node)
builder.add_edge(START, "chatbot")
builder.add_conditional_edges("chatbot", tools_condition)
builder.add_edge("tools", "chatbot")
agent_with_error_handler = builder.compile()
result = agent_with_error_handler.invoke({"messages": [{"role": "user", "content": "10除以1等于多少?"}]})
print(result)

增加消息记忆
对大模型的交互消息进行保存,这是实现多轮对话的关键。

在 LangChain 中,我们需要自行定义 ChatMessageHistory,并且自行保存每一轮的消息记录,然后在调用大模型时,将其作为参数传入。
LangGraph 中,实现消息记录的流程,也完整地封装到了 Agent 当中。
LangGraph 将消息记忆分为了短期记忆与长期记忆。
- 短期记忆是指 Agent 内部的记忆,用于当前对话中的历史记忆信息。LangGraph 将它封装为 CheckPoint。
- 长期记忆是指 Agent 外部的记忆,用第三方存储长久的保存用户级别或者应用级别的聊天信息。LangGraph 将它封装成 Store。

对于记忆管理,我认为 LangGraph 的管理方式或许比具体实现更有参考价值。
在具体实现时,LangGraph 都默认提供了 InMemorySaver 和 InMemoryStore,也同样都可以转移到其他外部存储当中。不过,短期记忆通常是代表那些会话级别的小内存,而长期记忆通常是代表那些用户级别或者应用级别的大内存。
短期记忆的内存比较紧张,所以需要更频繁地清理内存,并对已有的消息记录进行总结,从而减少内存占用。而长期记忆的内粗比较充足,所以不太需要频繁地清理内存。更需要关注的,是如何对已有的消息进行检索。
短期记忆 CheckPoint
在 LangGraph 的 Agent 中,只需要指定 checkpointer 属性,就可以实现短期记忆。具体传入的属性需要是 BaseCheckpointSaver 的子类。
LangGraph 中默认提供了 InMemorySaver,用于将短期记忆信息保存在内存中。当然,也可以采用 Redis、SQLLite 等三方存储来实现长期记忆。不过当前版本的 LangGraph 并没有提供具体的实现,需要自行实现。(如果不会写,交给 AI)
另外,使用 checkpointer 时,需要制定一个单独的 thread_id 来区分不同的对话。
python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.prebuilt import create_react_agent
from langchain_community.chat_models import ChatTongyi
from config.load_key import load_key
checkpointer = InMemorySaver()
def get_weather(city: str) -> str:
"""获取某个城市的天⽓"""
return f"城市:{city},天⽓⼀直都是晴天!"
# 初始化 LLM
llm = ChatTongyi(
model="qwen-plus",
api_key=load_key("DASHSCOPE_API_KEY"),
)
agent = create_react_agent(
model=llm,
tools=[get_weather],
checkpointer=checkpointer
)
# Run the agent
config = {
"configurable": {
"thread_id": "1" # 指定 thread_id
}
}
cs_response = agent.invoke(
{"messages": [{"role": "user", "content": "⻓沙天⽓怎么样?"}]},
config
)
print(cs_response)
# Continue the conversation using the same thread_id
bj_response = agent.invoke(
{"messages": [{"role": "user", "content": "北京呢?"}]},
config
)
print(bj_response)

从结果可以看到,当前对话中和大模型的每次交互记录,包括工具调用的信息,都保存在了短期记忆中。
当然,目前的实现是保存在内存中,所以程序结束后就释放了。生产环境中,LangGraph 建议是保存到外部存储当中,例如数据库、文件系统等。这样每次启动程序时,都可以从外部存储中加载历史记录。
短期记忆通常认为是比较紧张的,所以需要定期做清理,防止历史消息过多。
LangGraph 的 Agent 中,提供了一个 pre_model_hook 属性,可以在每次调用大模型之前触发。通过这个 hook,就可以来定期管理短期记忆。
LangGraph 中管理短期记忆的方法主要有 2 种:
- Summarization 总结:用大模型的方式,对短期记忆进行总结,然后再把总结的结果作为新的短期记忆
- Trimming 删除:直接把短期记忆中最旧的消息删除掉
LangGraph 提供了 SummarizationNode 函数,用于使用大模型的方式对短期记忆进行总结。
python
from langchain_classic.agents import tool
from langmem.short_term import SummarizationNode
from langchain_core.messages.utils import count_tokens_approximately, trim_messages
from langgraph.prebuilt import create_react_agent
from langgraph.prebuilt.chat_agent_executor import AgentState
from langgraph.checkpoint.memory import InMemorySaver
from typing import Any, Annotated
from langchain_community.chat_models import ChatTongyi
from config.load_key import load_key
# 定义工具
@tool
def get_weather(city: str) -> str:
"""获取某个城市的天气"""
return f"城市:{city},天气一直都是晴天!"
# 初始化 LLM
llm = ChatTongyi(
model="qwen-plus",
api_key=load_key("DASHSCOPE_API_KEY"),
)
# 使用大模型对历史信息进⾏总结
summarization_node = SummarizationNode(
token_counter=count_tokens_approximately,
model=llm,
max_tokens=384,
max_summary_tokens=128,
output_messages_key="llm_input_messages",
)
class State(AgentState):
# 注意:这个状态管理的作用是为了能够保存上⼀次总结的结果。这样就可以防⽌每次调⽤⼤模型时,都要重新总结历史信息。
# 这是一个比较常见的优化方式,因为大模型的调用是⽐较耗时的。
context: dict[str, Any]
checkpointer = InMemorySaver()
agent = create_react_agent(
model=llm,
tools=[get_weather],
pre_model_hook=summarization_node,
state_schema=State,
checkpointer=checkpointer,
)
另外,还提供了 trim_messages 函数,用于定期清理短期记忆。
python
# ============================================================
# 演示自定义 pre_model_hook:用 trim_messages 裁剪历史消息
# ============================================================
def pre_model_hook(state):
"""每次调用 LLM 前,裁剪消息历史,控制 token 数。"""
trimmed_messages = trim_messages(
state["messages"],
strategy="last",
token_counter=count_tokens_approximately,
max_tokens=384,
start_on="human",
end_on=("human", "tool"),
)
return {"llm_input_messages": trimmed_messages}
agent2 = create_react_agent(
model=llm,
tools=[get_weather],
pre_model_hook=pre_model_hook,
checkpointer=checkpointer,
)
# 模拟多轮对话
config = {"configurable": {"thread_id": "2"}}
result2 = agent2.invoke(
{"messages": [{"role": "user", "content": "长沙天气怎么样?"}]},
config,
)
print("=== 自定义 pre_model_hook 演示结果 ===")
print(result2)

实现了基础的短期记忆管理后,LangGraph 还提供了状态管理机制,用于保存处理过程中的中间结果。而且,这些状态数据,还可以在 Tools 工具中使用。
python
# ============================================================
# 演示 InjectedState:在工具中注入自定义状态
# ============================================================
from langgraph.prebuilt import InjectedState
class CustomState(AgentState):
user_id: str
@tool(return_direct=True)
def get_user_info(
state: Annotated[CustomState, InjectedState],
) -> str:
"""查询用户信息."""
user_id = state["user_id"]
return "user_123用户的姓名:楼兰。" if user_id == "user_123" else "未知用户"
agent2 = create_react_agent(
model=llm,
tools=[get_user_info],
state_schema=CustomState,
)
result = agent2.invoke({
"messages": [{"role": "user", "content": "查询用户信息"}],
"user_id": "user_123",
})
print("=== InjectedState 演示结果 ===")
print(result)

长期记忆
长期记忆通常认为是比较充足的记忆空间,因此使用时,可以比短期记忆更加粗犷,不太需要实时关注内存空间大小。
至于使用方式,和短期记忆差不太多。主要是通过 Agent 的 store 属性指定一个实现类就可以了。
与短期记忆最大的区别在于,短期记忆通过 thread_id 来区分不同的对话,而长期记忆则通过 namespace 来区分不同的命名空间。
python
from langchain_core.runnables import RunnableConfig
from langgraph.config import get_store
from langgraph.prebuilt import create_react_agent
from langgraph.store.memory import InMemoryStore
from langchain_core.tools import tool
# 定义长期存储
store = InMemoryStore()
# 添加⼀些测试数据。 users是命名空间,user_123是key,后⾯的JSON数据是value
store.put(
("users",),
"user_123",
{
"name": "楼兰",
"age": "33",
}
)
# 定义⼯具
@tool(return_direct=True)
def get_user_info(config: RunnableConfig) -> str:
"""查找用户信息"""
# 获取⻓期存储。获取到了后,这个存储组件可读也可写
store = get_store()
# store.put(
# ("users",),
# "user_456",
# {
# "name": "楼兰",
# "age": "33",
# }
# )
# 获取配置中的⽤户ID
user_id = config["configurable"].get("user_id")
user_info = store.get(("users",), user_id)
return str(user_info.value) if user_info else "Unknown user"
agent = create_react_agent(
model=llm,
tools=[get_user_info],
store=store
)
# Run the agent
agent.invoke(
{"messages": [{"role": "user", "content": "查找⽤户信息"}]},
config={"configurable": {"user_id": "user_123"}}
)

Human-in-the-loop人类监督
这也是 LangGraph 的 Agent 中非常核心的一个功能。
在 Agent 的工作过程中有一个问题是非常致命的。就是 Agent 可以添加 tools 工具,但是要不要调用工具,却完全是由 Agent 自己去决定的。
这就会导致 Agent 在面对一些问题时,可能会出现错误的判断。
为了解决这个问题,LangGraph 提供了 Human-in-the-loop 的功能,在 Agent 进行工具!调用的过程中,允许用户进行监督。这就需要中断当前的执行任务,等待用户输入后,再重新恢复任务。

在实现时,LangGraph 提供了 interrupt() 方法添加人类监督。监督时需要中断当前任务 ,所以通常是和 stream 流式方法配合使用。
python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt
from langgraph.prebuilt import create_react_agent
from langchain_core.tools import tool
from langchain_community.chat_models import ChatTongyi
from config.load_key import load_key
# An example of a sensitive tool that requires human review / approval
@tool(return_direct=True)
def book_hotel(hotel_name: str):
"""预定宾馆"""
response = interrupt(
f"正准备执⾏'book_hotel'⼯具预定宾馆,相关参数名: {{'hotel_name': {hotel_name}}}. "
"请选择OK,表示同意,或者选择edit,提出补充意⻅."
)
if response["type"] == "OK":
pass
elif response["type"] == "edit":
hotel_name = response["args"]["hotel_name"]
else:
raise ValueError(f"Unknown response type: {response['type']}")
return f"成功在 {hotel_name} 预定了⼀个房间."
checkpointer = InMemorySaver()
# 初始化 LLM
llm = ChatTongyi(
model="qwen-plus",
api_key=load_key("DASHSCOPE_API_KEY"),
)
agent = create_react_agent(
model=llm,
tools=[book_hotel],
checkpointer=checkpointer,
)
config = {
"configurable": {
"thread_id": "1"
}
}
for chunk in agent.stream(
{"messages": [{"role": "user", "content": "帮我在图灵宾馆预定⼀个房间"}]},
config
):
print(chunk)
print("\n")

执行完成后,会在 book_hotel 执行过程中,输出一个 Interrupt 响应,表示当前正在等待用户输入确认。
接下来,可以通过 Agent 提交一个 Command 请求,来继续完成之前的任务。需要注意的是,在这个示例中,Agent 只会一直等待用户输入。如果等待时间过长,后续请求就无法恢复了。
python
from langgraph.types import Command
for chunk in agent.stream(
# Command(resume={"type": "OK"}),
Command(resume={"type": "edit", "args": {"hotel_name": "三号宾馆"}}),
config
):
print(chunk)
print(chunk['tools']['messages'][-1].content)
print("\n")

多 Agent 应用构建
通常我们希望一个 Agent 能够专注干好一件事情。但是,如果是面对一些复杂的任务,我们可能需要多个 Agent 协同完成。例如一种典型的多 Agent 系统会是这样的:

由一个 Supervisor Agent 对任务进行分发。然后,交由另一个 Agent 来处理具体的事情。这样,这多个 Agent 就可以成为一个同时处理多个任务的系统。
LangGraph 中单独提供一个 langgraph-supervisor 依赖库来实现这种类型的多 Agent 系统。
当然,LangGraph 的核心是用 Graph 图的方式来实现多 Agent 协作。所以,这个库更多是作为基础了解。
bash
# 安装langgraph-supervisor依赖库
pip install langgraph-supervisor --upgrade
python
from langgraph.prebuilt import create_react_agent
from langgraph_supervisor import create_supervisor
from langchain_community.chat_models import ChatTongyi
from config.load_key import load_key
def book_hotel(hotel_name: str):
"""Book a hotel"""
return f"Successfully booked a stay at {hotel_name}."
def book_flight(from_airport: str, to_airport: str):
"""Book a flight"""
return f"Successfully booked a flight from {from_airport} to {to_airport}."
# 初始化 LLM
llm = ChatTongyi(
model="qwen-plus",
api_key=load_key("DASHSCOPE_API_KEY"),
)
flight_assistant = create_react_agent(
model=llm,
tools=[book_flight],
prompt="You are a flight booking assistant",
name="flight_assistant"
)
hotel_assistant = create_react_agent(
model=llm,
tools=[book_hotel],
prompt="You are a hotel booking assistant",
name="hotel_assistant"
)
supervisor = create_supervisor(
agents=[flight_assistant, hotel_assistant],
model=llm,
prompt=(
"You manage a hotel booking assistant and a"
"flight booking assistant. Assign work to them."
)
).compile()
for chunk in supervisor.stream(
{
"messages": [
{
"role": "user",
"content": "book a flight from BOS to JFK and a stay at McKittrick Hotel"
}
]
}
):
print(chunk)
print("\n")
LangGraph的Agent总结
Agent,这是 LangGraph 后续构建 Graph图的基础。但其实 Agent 并不是 LangGraph 框架当中独有的。甚至 Agent 并不是一种技术,而是我们设想的一种理想的大模型工作模式。那么到底什么是 Agent?或者说 AI 行业心目中理想的 Agent 应该是什么样子呢?LangGraph 实际上给我们提供了一种理解。而这个理解,或许比具体实现更重要。
这一章节,我们重点在演练 LangGraph 中 Agent 的功能。我们介绍了 LangGraph 的 Agent 功能,以及如何使用 LangGraph 的 Agent 来构建一个简单的应用。对于 LangGraph 框架来说,由于有了 LangChain 作为支撑,Agent 智能体或许并不是他的重点。后续,LangGraph 使用 Graph 图的方式协调管理多个 Agent,或许是更大的价值所在。
- 使用 LangGraph 构建智能体
- Agent 智能体增加 Tools 工具调用机制
- Agent 智能体消息记忆管理功能
- Human-In-Loop 人类监督功能
但是,LangGraph 对于 Agent 的功能封装却给大模型应用落地提供了非常好的思想指导。通过 Agent,我们不再需要关注应用的实现细节,而是可以更专注于应用的功能设计。而 Agent,绝不仅仅只是 LangGraph 所需要构建的,即便脱离 LangGraph 框架,如何构建一个能力强大又听话懂事的 Agent,或许是我们后续都需要思考的问题。
三、LangGraph接入MCP
在 2024 年底,Claude 大模型的开发公司 Anthropic 提出了一个 MCP(Model Context Protocol)协议。通过 MCP 协议,可以让应用程序以一种统一的方式向 LLM 大语言模型提供工具调用。
而随着几千个 MCP 服务突然冒出,百度、阿里等厂商也开始大规模接入 MCP,这个协议也迅速在互联网上引起轩然大波。按照哪些流量拍见风就是雨的性子,MCP 也着实被大吹特吹了一波。随着 MCP 服务越来越火爆,LangGraph 的 Agent 中也提供了 MCP 的集成。这次我们就分为几个部分,逐步深度拆解 MCP 服务。
- MCP 快速上手
- 理解 MCP 的 stdio 和 SSE 两种实现模式
- LangGraph 的 Agent 接入 MCP 服务
- 补充:自己开发一个 MCP 服务
1、MCP 快速上手
MCP 最神奇的地方就是只要做下简单的配置,就能完成很多复杂的工作。那这次,我们就先不来介绍 MCP 那些花里胡哨的概念,直接上手来玩玩 MCP。
首先,我们需要使用一个支持 MCP 的客户端工具。这类工具现在有很多,以后肯定也会越来越多。这次我们使用一个免费的。在 VS Code 中有一个开发插件,Cline。通过这个插件可以快速和 AI 大模型进行交互。
之前马克的技术工作坊也是用的 Cline。
然后,对 Cline 插件进行配置,让他访问国内的模型,这样就不需要科学上网了。

点击这个 MCP Server 按钮,通过这里,就可以开启 MCP 的体验之路。
以高德地图提供的 MCP 服务为例。高德地图开放平台提供了非常详细的 MCP 接入介绍:概述-MCP Server | 高德地图API。只需要在高德开放平台注册账号,登录后,在左侧的菜单中选择"应用管理→我的应用",然后创建应用,就可以申请一个 API-KEY。
有了这个 API-KEY 之后,在 MCP Server 按钮的左侧,选择MCP Server。在 MCP 的配置文件中,写入以下内容:
json
{
"mcpServers":
"amap-ampa-sse":{
"url": "https://mcp.amap.com/sse?key=ef159aab7a695f4f647b35908731a575"
}
}
}
配置完成,右侧就能看到这个 MCP 服务被安装上来了。

接下来,在 Cline 中,如果你想询问和地图相关的一些问题,那么,Cline 在调用大模型的过程中,就会自动调用高德地图的 MCP 服务,获得地图相关的信息。

到这里,我们就完成了 MCP 服务的初体验。只需要简单配置一个 JSON 文件,就可以给大模型提供更多扩展的功能。
当然,除了这里演示的高德地图的 MCP 服务外,还有很多五花八门的 MCP 服务。甚至还有很多 MCP 的信息聚合网站。比如MCP Server(MCP 服务器)上,目前就聚合了好几千个各种各样的 MCP 服务。通过这些服务,我们可以让 AI 大模型完成聊天以外的各种各样的功能。包括发送邮件、浏览网页等。甚至直接操作本地文件也可以。还有阿里云百炼平台大模型服务平台百炼控制台更是将各种 MCP 服务聚合成了很多独立的服务。只要开通对应的服务后,就可以在阿里云百炼的智能应用中,用图形化的方式直接集成这些 MCP 服务。
通过 MCP 服务,可以让大模型不再只是扮演一个大脑的角色,而是拥有了四肢,直接替代人的工作能力。这样看来,MCP 确实给大模型带来了一次质的飞跃。但是,事实情况真的是这样吗?
作为纯小白,或许你只能跟在别人身后,听风就是雨。但是,作为程序员,我们是有能力更全面地去理解 MCP 的。
2、详细梳理MCP工作机制
MCP 协议,全称 Model Context Protocol,中文翻译是模型上下文协议。MCP 协议是由 Anthropic 公司提出的,是一个专门用于与 AI 大语言模型进行交互的协议。但是本质上,MCP 协议只是运行应用程序以一种统一的方式向大语言模型提供 Function Calling 函数调用。而 Function Calling 是很多大语言模型自身就提供的一种功能机制。
例如,通过下面的案例,我们就可以很简单的给大模型提供路线规划的能力。
python
from config.load_key import load_key
from langchain_openai import ChatOpenAI
# 构建阿⾥云百炼⼤模型客户端
llm = ChatOpenAI(
model="qwen-plus",
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
openai_api_key=load_key("DASHSCOPE_API_KEY"),
)
import datetime
from langchain.tools import tool
# 定义工具 注意要添加注释
@tool(description="规划⾏⻋路线")
def get_route_plan(origin_city: str, target_city: str):
"""规划⾏⻋路线
Args:
origin_city: 出发城市
target_city: ⽬标城市
"""
result = f"从城市 {origin_city} 出发,到⽬标城市 {target_city} ,使⽤意念传送,只需要三分钟即可到达。"
print(">>>> get_route_plan >>>>>" + result)
return result
# ⼤模型绑定⼯具
llm_with_tools = llm.bind_tools([get_route_plan])
# ⼯具容器
all_tools = {"get_route_plan": get_route_plan}
# 把所有消息存到⼀起
query = "帮我规划⼀条从⻓沙到桂林的⾃驾路线"
messages = [query]
# 询问大模型。大模型会判断需要调用工具,并返回一个工具调⽤请求
ai_msg = llm_with_tools.invoke(messages)
print(ai_msg)
print("--------")
messages.append(ai_msg)
# 打印需要调⽤的⼯具
print(ai_msg.tool_calls)
print("----")
if ai_msg.tool_calls:
for tool_call in ai_msg.tool_calls:
selected_tool = all_tools[tool_call["name"].lower()]
tool_msg = selected_tool.invoke(tool_call)
messages.append(tool_msg)
print(llm_with_tools.invoke(messages).content)
询问大模型,每次询问的结果都是不一样的,这是一次典型的执行结果:

这个案例的工作效果和 MCP 是如出一辙的。只不过,高德的 MCP 服务给的数据比较靠谱,所以大模型直接采纳了他的数据,而我们的意念传送的数据显然不是很靠谱,所以大模型虽然参考了我们的方案,但是最终并没有全部采纳我们的结果。
从这里也能看出这种工作机制在处理一些问题时暴露出的一些核心问题。就是要不要调用工具,本身是大模型说了算。而最后不管工具给出什么样的答案,最终大模型回复什么内容,也还是大模型说了算。至于工具执行过程中做了什么事情,大模型完全不管。
了解了这个案例后,就可以看出,MCP 其实只是大模型工作机制的一种应用层的协议。

MCP 协议虽然极具大模型的应用特色,但是本质上,MCP 协议本身并不包含任何具体的工具实现。他只用协议的形式规定了应用程序如何向大模型提供函数调用的能力。
至于为什么 MCP 服务使用起来这么简单,其实是 Cline 这样的工具封装了客户端的实现能力。这是工具的一种简化实现,和 MCP 协议本身是没有太大关系的。
这个关系就好像我们以往使用 HTTP 协议访问各种各样的网站一样的。我们这些普通人,可以在完全不用了解 HTTP 协议是个啥,只要简单的使用浏览器就可以访问网站。但是,这并不意味着 HTTP 就是一个简单的协议。在 HTTP 协议层面,要考虑的问题也肯定不能只是简单的保证数据传输,而需要对网络传输的规范性、安全性等等各个方面做出很多的设计。
从这个功能层面上来说,MCP 协议和 HTTP 协议本质上是相同的。他基于大模型的 Function Call 工具实现,只不过是通过协议的方式定义了这些工具要如何工作,这样可以极大的提升各种工具的复用能力。但是,作为一个协议,MCP 要考虑的事情,同样不应该只是考虑这些工具功能如何实现,还需要在各个方面保证这些工具,不会出乱子。
3、拆解MCP的两种实现方式(SSE和STDIO)
MCP 还没听明白,怎么又冒出 SSE 和 STDIO 了?不用担心,还是老规矩,直接上实例。
还是以我们之前使用的高德地图的 MCP 服务为例。在高德地图开放平台的介绍中,提供了两种接入高德地图的 MCP 配置方式。
一种是我们之前使用过的,配置一个网站地址,这就是典型的 SSE 实现机制。
json
{
"mcpServers": {
"amap-amap-sse": {
"url": "https://mcp.amap.com/sse?key=你在高德官网上申请的key"
}
}
}
另一种,没有使用过的 STDIO 的配置方式是这样的:
json
"amap-maps": {
"command": "npx",
"args": [
"-y",
"@amap/amap-maps-mcp-server"
],
"env": {
"AMAP_MAPS_API_KEY": "高德地图key"
}
}
这种方式配置方式需要在本地安装 Node.js。很显然,是通过在本地执行 npx 指令,执行了一个应用程序,从而获得高德地图的数据。
至于这个数据是如何获得的?是调用远端高德地图的服务获得的?还是读取本地某个神秘文件获取的?这就只有在高德地图提供的 Node.js 源码 @amap/amap-maps-mcp-server 中才能知道了。
从这个案例中我们就能理解出 SSE 和 STDIO 到底是怎样的工作机制。
- SSE:其实是一种基于 HTTP 协议实现的长连接协议,只不过 SSE 协议是一种从服务端向客户端单向推送数据的长连接协议。也就是高德地图需要提供一个 HTTP 服务,然后客户端可以和这个 HTTP 服务建立一个长连接,这样客户端就可以不断地访问高德地图的 HTTP 服务,获得高德地图的服务数据。这时候的工作机制其实和以往我们熟悉的基于 HTTP 的工作机制本质上是很像的。只是服务端的性能压力会大一点而已。
- STDIO:这种工作机制的本质是在客户端本地执行一个应用程序,然后通过应用程序获得对应的结果。这时候 MCP 的核心问题就出来了。MCP 的服务是由 MCP 的服务提供者设计的,但是执行却是在客户端的机器执行。也就是说,这给服务提供者提供了一种操作客户端机器的机会。这里面会带来多少安全问题?修改以下你本地的文件,或者给你植入一个病毒程序,或者......大家可以发挥一下自己的想象。
4、Agent接入MCP服务
LangChain 中提供了一个新的功能模块:langchain-mcp-adapters 来支持 MCP 服务
bash
# 安装对应的依赖
pip install langchain-mcp-adapters
接下来,接入 MCP 服务也很简单,只要增加 MCP 的配置文件就行:
python
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
from config.load_key import load_key
from langchain_community.chat_models import ChatTongyi
# 构建阿⾥云百炼⼤模型客户端
llm = ChatTongyi(
model="qwen-plus",
api_key=load_key("DASHSCOPE_API_KEY"),
)
# 相⽐Cline客户端配置,只要增加transport属性即可。不过测试stremaable_http有问题。不知道是不是版本的原因。
client = MultiServerMCPClient(
{
"amap-amap-sse": {
"url": "https://mcp.amap.com/sse?key=ef159aab7a695f4f647b35908731a575",
"transport": "sse"
},
# "amap-maps": {
# "command": "npx",
# "args": [
# "-y",
# "@amap/amap-maps-mcp-server"
# ],
# "env": {
# "AMAP_MAPS_API_KEY": "451ad40d0e39453600f2a305e31eabe4"
# },
# "transport":"stdio"
# }
}
)
async def main():
tools = await client.get_tools()
agent = create_react_agent(
model=llm,
tools=tools
)
response = await agent.ainvoke(
{"messages": [{"role": "user", "content": "帮我规划一条从成都光明城市小区到大熊猫基地的自驾路线"}]}
)
print(response)
if __name__ == "__main__":
asyncio.run(main())

5、手写实现一个MCP服务
如何实现一个MCP服务?MCP的官网上提供了一系列的SDK来辅助实现 MCP 的客户端和服务端。官网地址:What is the Model Context Protocol (MCP)? - Model Context Protocol
SDK 是 Software Development Kit(软件开发工具包)的缩写。
简单来说,SDK 就是一套"现成的工具箱",里面包含:
- 库/框架:封装好的代码,你可以直接调用,不用从零写起
- API 接口:和某个平台/服务通信的标准方法
- 文档和示例:告诉你怎么用这些工具
- 调试工具:帮你排查问题
MCP 官方提供了 SDK,意思是他们已经帮你把"如何建立连接、如何收发消息、如何注册工具"这些底层逻辑都写好了。你只需要调用 SDK 里的函数,就能快速搭建一个 MCP 客户端或服务端,而不用自己去研究底层协议细节。
**类比理解:**如果你要盖房子,SDK 就是给你的"预制构件 + 施工手册",你不需要自己烧砖、炼钢,直接用现成的零件拼装就行。
那么接下来的事情就简单了,我们使用 Python 客户端来实现一个 MCP 服务看看:
-
手写SSE实现
首先需要安装 MCP 依赖
bashpip install mcp然后,就可以参照官网案例,快速实现一个 MCP 服务(感觉与之前学的马克那个几乎一样)
pythonfrom mcp.server.fastmcp import FastMCP mcp = FastMCP("roymcpdemo") @mcp.tool() def add(a: int, b: int) -> int: """Add two numbers together.""" print(f"roy mcp demo called : add({a}, {b})") return a + b @mcp.tool() def weather(city: str): """获取某个城市的天⽓ Args: city: 具体城市 """ return "城市" + city + ",今天天⽓不错" @mcp.resource("greeting://{name}") def greeting(name: str) -> str: """Greet a person by name.""" print(f"roy mcp demo called : greeting({name})") return f"Hello, {name}!" if __name__ == "__main__": # 以sse协议暴露服务。 mcp.run(transport='sse') # 以stdio协议暴露服务。 # mcp.run(transport='stdio')这里就用 @mcp.tool() 注解,快速声明了 2 个服务。
执行这个 python 代码后,就可以启动一个服务。接下来,就可以在 Cline 客户端中配置对应的客户端服务了。
json"roymcpdemo" : { "url": "http://127.0.0.1:8000/sse" }配置完成后,就可以在 Cline 中看到我们声明的工具和资源了。

这就是按照 MCP 的 SSE 提供的一种服务实现。
另外,跟着服务端的日志,会发现:Cline 客户端之所以能够发现这些服务,是因为发送了一些请求,获取到工具声明信息的。

-
切换成STDIO实现
要切换成 STDIO 的协议,也很简单。对于服务端,只需要改最后一行代码。
pythonif __name__ == "__main__": # 以sse协议暴露服务。 # mcp.run(transport='sse') # 以stdio协议暴露服务。 mcp.run(transport='stdio')这时候,就需要一个客户端程序,来访问并调用服务端提供的这些功能。整体上,也还是处理这几个请求。
pythonfrom mcp import StdioServerParameters, stdio_client, ClientSession import mcp.types as types server_params = StdioServerParameters( command="python", # 这个13.py就是刚才的服务端代码 args=["C:\\study\\LangGraph_Projects\\langraph-demo\\13.py"], env=None ) async def handle_sampling_message(message: types.CreateMessageRequestParams) -> types.CreateMessageResult: print(f"sampling message: {message}") return types.CreateMessageResult( role="assistant", content=types.TextContent( type="text", text="Hello,world! from model" ), model="qwen-plus", stopReason="endTurn" ) async def run(): async with stdio_client(server_params) as (read, write): async with ClientSession(read, write, sampling_callback=handle_sampling_message) as session: await session.initialize() prompts = await session.list_prompts() print(f"prompts: {prompts}") tools = await session.list_tools() print(f"tools: {tools}") resources = await session.list_resources() print(f"resources: {resources}") result = await session.call_tool("weather", {"city": "北京"}) print(f"result: {result}") if __name__ == "__main__": import asyncio asyncio.run(run())
接下来,也可以用同样的方式尝试在 LangGraph 中接入服务。
当然,由于以来服务端的代码实现,所以不能简单的配置一个 python 指令执行,需要打包成 nodejs 依赖启动,后续就不再做介绍了。
6、章节总结
通过深度演示 MCP 服务的客户端和服务端的交互过程,让大家对 MCP 服务有了初步的了解。在深度使用更多的 MCP 服务的同时,合理地看待 MCP 的安全问题。虽然 MCP 最大的意义在于简化客户端的调用过程,让我们以极小的代价快速接入更多的外部服务,但是并不代表 MCP 服务就是安全的。
四、深度理解LangGraph核心-Graph
在了解了 LangGraph 中如何构建 Agent 智能体之后,接下来就要进入 LangGraph 的重头戏------Graph 了。Graph 是 LangGraph 的核心,它以有向无环图的方式来串联多个 Agent,构建更复杂的 Agent 大模型应用,形成更复杂的工作流。并且提供了很多产品级的特性,保证这些应用可以更稳定高效的执行。
Graph 是 LangGraph 的基本构建模块,它是一个有向无环图(DAG),用于描述任务之间的依赖关系。
主要包含三个基本的元素:
- State:在整个应用当中共享的一种数据结构
- Node:一个处理数据的节点。LangGraph 中通常是一个 Python 的函数,以 State 为输入,经过一些操作后,返回更新后的 State
- Edge:表示 Node 之前的依赖关系。LangGraph 中通常也是一个 Python 函数,根据当前 State 来决定下来执行哪个 Node。
接下里,用一个最简单的案例,来看下 Graph 的基本用法。
bash
# 安装依赖
pip install -U langgraph
python
from typing import TypedDict
from langgraph.constants import END, START
from langgraph.graph import StateGraph
class InputState(TypedDict):
user_input: str
class OutputState(TypedDict):
graph_output: str
class OverallState(TypedDict):
foo: str
user_input: str
graph_output: str
class PrivateState(TypedDict):
bar: str
def node_1(state: InputState) -> OverallState:
# Write to OverallState
return {"foo": state["user_input"] + ">学院"}
def node_2(state: OverallState) -> PrivateState:
# Read from OverallState, write to PrivateState
return {"bar": state["foo"] + ">非常"}
def node_3(state: PrivateState) -> OutputState:
# Read from PrivateState, write to OutputState
return {"graph_output": state["bar"] + ">靠谱"}
# 构建图
builder = StateGraph(OverallState, input_schema=InputState, output_schema=OutputState)
# 添加Node
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_node("node_3", node_3)
# 添加Edge
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", "node_3")
builder.add_edge("node_3", END)
# 编译图
graph = builder.compile()
# 调用图
result = graph.invoke({"user_input": "图灵"})
print(result)

这个案例中,请求的参数从固定的 START 传入,依次经过三个节点处理,每个节点的处理结果都会被保存到不同的 state 当中,最后进入 END 节点结束。
可以看到,一个 Graph 中,可以通过对 Node 和 Edge 的灵活组合,形成各种复杂的流程。接下来,我们就是要接入 Agent,来完成各种复杂的任务。
在构建复杂任务之前,我们先来仔细看看 Graph 中的这三个主要组件。
1、State 状态
State 是所有节点共享的状态,它是一个字典,包含了所有节点的状态。有几个需要注意的地方:
-
State 形式上,可以是 TypedDic 字典,也可以是 pydantic 中的一个 BaseModel。例如:
pythonfrom pydantic import BaseModel # The overall state of the graph (this is the public state shared across nodes) class OverallState(BaseModel): a: str这两种实现,本质上没有太多的区别。
-
State 中定义的属性,通常不需要指定默认值。如果需要默认值,可以通过在 START 节点后,定义一个 node 来指定默认值
javadef node(state: OverallState): return {"a": "goodbye"} -
State 中的属性,除了可以修改的值外,也可以定义一些操作。来指定如何更新 State 中的值。例如:
python# add_messages,一个专门用于消息列表的归并函数 from langgraph.graph.message import add_messages # State继承自TypeDict,是LangGraph中节点之间共享的全局状态结构 class State(TypedDict): messages: Annotated[list[AnyMessage], add_messages] list_field: Annotated[list[int], add] extra_field: int三个字段详解
字段 类型 Reducer 行为 messageslist[AnyMessage]add_messages智能合并消息 list_fieldlist[int]add(即+)列表拼接 extra_fieldint无 直接覆盖
Annotated[T, reducer]的含义Annotated的第二个参数 是 reducer 函数 ,它告诉 LangGraph:当多个节点都更新同一个字段时,如何合并新旧值。旧状态值 + 节点返回的新值 → reducer(旧, 新) → 最终状态值
字段 1:
messages+add_messagespythonmessages: Annotated[list[AnyMessage], add_messages]add_messages是 LangGraph 内置的智能 reducer,它:- 追加新消息到列表末尾
- 去重 :若新消息的
id已存在,则替换旧消息(而非重复添加) - 支持
HumanMessage、AIMessage、ToolMessage等各种消息类型
python# 旧状态 messages = [HumanMessage("你好", id="1")] # 节点返回 {"messages": [AIMessage("我很好", id="2")]} # 合并后 messages = [HumanMessage("你好", id="1"), AIMessage("我很好", id="2")]
字段 2:
list_field+addpythonlist_field: Annotated[list[int], add]add就是 Python 的operator.add,即+运算符,对列表执行拼接:python# 旧状态 list_field = [1, 2] # 节点返回 {"list_field": [3, 4]} # 合并后:[1, 2] + [3, 4] list_field = [1, 2, 3, 4]
字段 3:
extra_field(无 reducer)pythonextra_field: int没有指定 reducer,LangGraph 默认行为是直接覆盖:
python# 旧状态 extra_field = 10 # 节点返回 {"extra_field": 99} # 合并后:直接替换 extra_field = 99
整体作用
节点 A ──┐ ├──► State(messages, list_field, extra_field) ──► 节点 B ──► ... 节点 C ──┘这个
State类定义了整个 LangGraph 工作流的共享内存结构 ,通过 reducer 机制,确保多个节点并发写入同一字段时数据能被正确合并,而不是互相覆盖。此时,如果有一个 node,返回了 State 中更新的值,那么
messages和list_field的值就会添加到原有的旧集合中,而extra_field的值则会被替换。pythonfrom langchain_core.messages import AnyMessage, AIMessage from langgraph.graph import StateGraph from langgraph.graph.message import add_messages from typing import Annotated, TypedDict from operator import add # ── 1. 定义状态结构 ────────────────────────────────────── class State(TypedDict): messages: Annotated[list[AnyMessage], add_messages] list_field: Annotated[list[int], add] extra_field: int # ── 2. 定义节点函数 ────────────────────────────────────── def node1(state: State): new_message = AIMessage("Hello!") return { "messages": [new_message], "list_field": [10], "extra_field": 10, } def node2(state: State): new_message = AIMessage("LangGraph!") return { "messages": [new_message], "list_field": [20], "extra_field": 20, } # ── 3. 构建并编译图 ────────────────────────────────────── graph = ( StateGraph(State) .add_node("node1", node1) .add_node("node2", node2) .set_entry_point("node1") .add_edge("node1", "node2") .compile() ) # ── 4. 运行图 ──────────────────────────────────────────── input_message = {"role": "user", "content": "Hi"} result = graph.invoke({ "messages": [input_message], "list_field": [1, 2, 3], }) print(result) # 取消注释可查看更易读的输出: for message in result["messages"]: message.pretty_print() # print(result["extra_field"])
在 LangGraph 的应用当中,State 通常都会要保存聊天消息。为此,LangGraph 中还提供了一个 langgraph.graph.MessageState,可以用来快速保存消息。
它的声明方式是这样的:
pythonclass MessagesState(TypedDict): messages:Annotated[list[AnyMessage], add_messages]然后,对于 Messages,也可以用序列化的方式来表明,例如下面两种方式都是可以的:
json{"messages": [HumanMessage(content="message")]} {"messages": [{"type": user, "content": "message"}]}
2、Node节点
Node 是图中的一个处理数据的节点。也有以下几个需要注意的地方:
- 在 LangGraph 中,Node 通常是一个 Python 函数,它接受一个 State 对象作为输入,返回一个 State 对象作为输出。
- 每个 Node 都有一个唯一的名称,通常是一个字符串。如果没有提供名称,LangGraph 会自动生成一个和函数名一样的名称。
- 在具体实现时,通常包含两个就具体的参数,第一个是 State,这个是必选的。第二个是一个可选的配置项 config。这里面包含了一些节点运行的配置参数。
- LangGraph 对每个 Node 提供了缓存机制。只要 Node 的传入参数相同,LangGraph 就会优先从缓存当中获取 Node 的执行结果,从而提升 Node 的运行速度。
python
import time
from typing import TypedDict
from langchain_core.runnables import RunnableConfig
from langgraph.constants import START, END
from langgraph.graph import StateGraph
from langgraph.types import CachePolicy
from langgraph.cache.memory import InMemoryCache # LangGraph 自带的缓存,非 LangChain
# ── 1. 定义状态结构 ──────────────────────────────────────────────────────────
class State(TypedDict):
number: int
user_id: str
# ── 2. 定义运行时配置结构 ──────────────────────────────────────────────────────
class ConfigSchema(TypedDict):
user_id: str
# ── 3. 定义节点函数 ────────────────────────────────────────────────────────────
def node_1(state: State, config: RunnableConfig):
time.sleep(3) # 模拟耗时操作
user_id = config["configurable"]["user_id"]
return {
"number": state["number"] + 1,
"user_id": user_id,
}
# ── 4. 构建图结构 ──────────────────────────────────────────────────────────────
builder = StateGraph(State, config_schema=ConfigSchema)
# 为 node1 设置 5 秒 TTL 缓存策略
builder.add_node("node1", node_1, cache_policy=CachePolicy(ttl=5))
builder.add_edge(START, "node1")
builder.add_edge("node1", END)
# 编译图,注入内存缓存
graph = builder.compile(cache=InMemoryCache())
# ── 5. 第一次调用(缓存未命中,实际执行) ─────────────────────────────────────
print(graph.invoke(
{"number": 5},
config={"configurable": {"user_id": "123"}},
stream_mode="updates",
))
# 输出: [{'node1': {'number': 6, 'user_id': '123'}}]
# ── 6. 第二次调用(缓存命中,跳过执行) ───────────────────────────────────────
# ⚠️ 注意:user_id 改为 "456",但缓存 key 仅基于节点入参 state,config 不参与缓存 key
print(graph.invoke(
{"number": 5},
config={"configurable": {"user_id": "456"}},
stream_mode="updates",
))
# 输出: [{'node1': {'number': 6, 'user_id': '123'}, 'metadata': {'cached': True}}]

逐层解释
1.
State--- 图的数据容器
pythonclass State(TypedDict): number: int user_id: str图中所有节点共享这个状态对象,节点读取它、也可以更新它。
2.
ConfigSchema--- 运行时配置
pythonclass ConfigSchema(TypedDict): user_id: str通过
config={"configurable": {...}}在 invoke 时动态传入,不属于状态的一部分,用于控制运行行为(如切换用户、模型等)。
3.
node_1--- 核心节点
参数 来源 说明 state图状态 读取 number,返回+1configinvoke 时传入 从 configurable中取user_id
time.sleep(3)模拟真实场景中耗时的 LLM 调用或 API 请求。
4.
CachePolicy(ttl=5)--- 节点级缓存
pythonbuilder.add_node("node1", node_1, cache_policy=CachePolicy(ttl=5))
- 缓存 key = 节点的输入 state (
{"number": 5})- TTL = 5 秒,5 秒内相同 state 输入直接返回缓存结果
⚠️ 核心陷阱:
config不参与缓存 key
第1次: state={"number":5}, user_id="123" → 实际执行,结果缓存 第2次: state={"number":5}, user_id="456" → state 相同 → 命中缓存 → 返回旧结果(user_id 仍是 "123")这是该代码最关键的行为:
config里的参数不影响缓存 key ,所以即使换了user_id,只要state相同,就会返回缓存的旧数据,并附带'metadata': {'cached': True}标志。
实际应用建议
如果你的节点输出依赖于 config 中的参数 (如 user_id),应该将该参数也放入
state,让它参与缓存 key 的计算,避免不同用户拿到同一份缓存结果。
对于 Node,LangGraph 除了提供缓存机制,还提供了重试机制。可以针对单个节点指定,例如:
python
from langgraph.types import RetryPolicy
builder.add_node("node1", node_1, retry=RetryPolicy(max_attempts=4))
另外,也可以针对某⼀次任务调用指定,例如:
python
print(graph.invoke(xxxxx, config={"recursion_limit":25}))
3、Edge 边
在 Graph 图中,通过 Edge(边)把 Node(节点)连接起来,从而决定 State 应该如何在 Graph 中传递。LangGraph 中也提供了非常灵活的构建方式。
-
普通 Edge 和 EntryPoint
Edge 通常是用来把两个 Node 连接起来,形成逻辑处理路线。例如:
graph.add_edge("node_1", "node_2")。LangGraph 中提供了 2 个默认的 Node,START和END,用来作为 Graph 的入口和出口。同时,也可以自行指定 EntryPoint。例如:
pythonbuilder = StateGraph(State) builder.set_entry_point("node1") builder.set_finish_point("node2") -
条件 Edge 和 EntryPoint
我们也可以添加带有条件判断的 Edge 和 EntryPoint,用来动态构建更复杂的工作流程。具体实现时,可以指定一个函数,函数的返回值就可以是下一个 Node 的名称。
pythonfrom typing import TypedDict from langchain_core.runnables import RunnableConfig from langgraph.constants import START, END from langgraph.graph import StateGraph # 配置状态 class State(TypedDict): number: int def node_1(state: State, config: RunnableConfig): return {"number": state["number"] + 1} builder = StateGraph(State) # 添加节点 builder.add_node("node1", node_1) def routing_func(state: State) -> str: if state["number"] > 5: return "node1" else: return END builder.add_edge("node1", END) builder.add_conditional_edges(START, routing_func) graph = builder.compile() print(graph.invoke({"number": 7}))
python# 补充看⼀下Graph的结构 from IPython.display import Image, display display(Image(graph.get_graph().draw_mermaid_png()))
另外,如果不想在路由函数中写⼊过多具体的节点名称,也可以在函数中返回⼀个⾃定义的结果,然后将这个结果解析到某⼀个具体的 Node 上。例如
pythondef routing_func (state:State) -> bool: if state["number"] > 5: return True else: return False builder.add_conditional_edges( START, routing_func,{True: "node_a", False: "node_b"}) -
Send 动态路由
在条件边中,如果希望一个 Node 后同时路由到多个 Node,就可以返回 Send 动态路由的方式实现。
Send 对象可传入 2 个参数,第一个是下一个 Node 的名称,第二个是 Node 的输入。
pythonfrom operator import add from typing import TypedDict, Annotated from langgraph.constants import START, END from langgraph.graph import StateGraph from langgraph.types import Send # 配置状态 class State(TypedDict): messages: Annotated[list[str], add] class PrivateState(TypedDict): msg: str def node_1(state: PrivateState) -> State: res = state["msg"] + "!" return {"messages": [res]} builder = StateGraph(State) builder.add_node("node1", node_1) def routing_func(state: State): result = [] for message in state["messages"]: result.append(Send("node1", {"msg": message})) return result # 通过路由函数,将消息中每个字符串分别传入node1处理。 builder.add_conditional_edges(START, routing_func, ["node1"]) builder.add_edge("node1", END) graph = builder.compile() print(graph.invoke({"messages": ["hello", "world", "hello", "graph"]})) # {'messages': ['hello', 'world', 'hello', 'graph', 'hello!', 'world!', 'hello!', 'graph!']}
python# 补充看⼀下Graph的结构 from IPython.display import Image, display display(Image(graph.get_graph().draw_mermaid_png()))
-
Command 命令
通常,Graph 中一个典型的业务步骤是 State 进入一个 Node 处理。在 Node 中先更新 State 状态,然后再通过 Edges 传递给下一个 Node。如果希望将这 2 个步骤合并为一个命令,那么还可以使用 Command 命令。
pythonfrom operator import add from typing import TypedDict, Annotated from langgraph.constants import START, END from langgraph.graph import StateGraph from langgraph.types import Command # 配置状态 class State(TypedDict): messages: Annotated[list[str], add] def node_1(state: State): new_message = [] for message in state["messages"]: new_message.append(message + "!") return Command( goto=END, update={"messages": new_message} ) builder = StateGraph(State) builder.add_node("node1", node_1) # node1中通过Command同时集成了更新State和指定下个Node builder.add_edge(START, "node1") graph = builder.compile() print(graph.invoke({"messages": ["hello", "world", "hello", "graph"]})) # {'messages': ['hello', 'world', 'hello', 'graph', 'hello!', 'world!', 'hello!','graph!']}
4、子图
在 LangGraph 中,一个 Graph 除了可以单独使用,还可以作为一个 Node,嵌入到另一个 Graph 中。这种用法就称为子图。通过子图,我们可以更好的重用 Graph,构建更复杂的工作流。尤其是在构建多 Agent 时,非常有用。在大型项目中,通常都是由一个团队专门开发 Agent,再通过其他团队来完成 Agent 整合。
使用子图时,基本和使用 Node 没有太多区别。唯一需要注意的是,当触发了 SubGraph 代表的 Node 后,实际上是相当于重新调用了一次 subgraph.invoke(state) 方法。
python
# subgraph与graph使⽤相同State
from operator import add
from typing import TypedDict, Annotated
from langgraph.constants import END
from langgraph.graph import StateGraph, MessagesState, START
class State(TypedDict):
messages: Annotated[list[str], add]
# Subgraph
def sub_node_1(state: State) -> MessagesState:
return {"messages": ["response from subgraph"]}
subgraph_builder = StateGraph(State)
subgraph_builder.add_node("sub_node_1", sub_node_1)
subgraph_builder.add_edge(START, "sub_node_1")
subgraph_builder.add_edge("sub_node_1", END)
subgraph = subgraph_builder.compile()
# Parent graph
builder = StateGraph(State)
builder.add_node("subgraph_node", subgraph)
builder.add_edge(START, "subgraph_node")
builder.add_edge("subgraph_node", END)
graph = builder.compile()
print(graph.invoke({"messages": ["hello subgraph"]}))
# 结果hello subgraph会出现两次。这是因为在subgraph_node中默认调⽤了⼀次subgraph.invoke(state)⽅法。主图⾥也调⽤了⼀次invoke。这就会往state中添加两次语句
# {'messages': ['hello subgraph', 'hello subgraph', 'response from subgraph']}

5、图的Stream支持
和调用大模型相似,Graph 除了可以通过 invoke 方法进行直接调用外,也支持通过 stream() 方法进行流式调用。不过大模型的流式调用是依次返回大模型响应的 Token。而 Graph 的流式输出则是依次返回 State 的数据处理步骤。Graph 提供了 stream() 方法进行同步的流式调用,也提供了 astream() 方法进行异步的流式调用。
python
for chunk in graph.stream({"messages": ["hello subgraph"]}, stream_mode="debug"):
print(chunk)
# {'subgraph_node': {'messages': ['hello subgraph', 'response from subgraph']}}

bash
{'step': 1, 'timestamp': '2026-06-16T23:34:06.275846+00:00', 'type': 'task', 'payload': {'id': '5b7d957d-7474-4c71-ff7f-b6e0324152be', 'name': 'subgraph_node', 'input': {'messages': ['hello subgraph']}, 'triggers': ('branch:to:subgraph_node',)}}
{'step': 1, 'timestamp': '2026-06-16T23:34:06.276265+00:00', 'type': 'task_result', 'payload': {'id': '5b7d957d-7474-4c71-ff7f-b6e0324152be', 'name': 'subgraph_node', 'error': None, 'result': {'messages': ['hello subgraph', 'response from subgraph']}, 'interrupts': []}}
LangGraph支持几种不同的 stream mode:
- values:在图的每一步之后流式传输状态的完整值.
- updates:在图的每一步之后,将更新内容流式传输到状态。如果在同一步骤中进行了多次更新(例如,运行了多个节点),这些更新将分别进行流式传输。
- custom:从图节点内部流式传输自定义数据。通常用于调试。
- messages:从任何调用大语言模型(LLM)的图节点中,流式传输二元组(LLM 的 Token,元数据)
- debug:在图的执行过程中尽可能多地传输信息。用得比较少。
values、updates、debug 输出模式,使用之前案例验证,就能很快感受到其中的区别。
messages 输出模式,由于在之前案例中并没有调用大模型,所以不会有输出结果。
而custom输出模式,可以自定义输出内容。在 Node 节点内或者 Tools 工具内,通过 get_stream_writer() 方法获取一个 StreamWriter 对象,然后使用 write() 方法将自定义数据写入流中。
python
from typing import TypedDict
from langgraph.config import get_stream_writer
from langgraph.graph import StateGraph, START
class State(TypedDict):
query: str
answer: str
def node(state: State):
writer = get_stream_writer()
writer({"自定义key": "在节点内返回自定义信息"})
return {"answer": "some data"}
graph = (
StateGraph(State)
.add_node(node)
.add_edge(START, "node")
.compile()
)
inputs = {"query": "example"}
# Usage
for chunk in graph.stream(inputs, stream_mode="custom"):
print(chunk)

最后,在 LangChain 中,构建 LLM 对象时,大都支持 desable_streaming 属性,禁止流式输出。例如:
python
llm = ChatOpenAI(model="", disable_streaming=True)
llm = ChatOpenAI(model="", disable_streaming=True)
6、总结
在这一章节,我们详细演练了 LangGraph 中的 Graph 构建以及工作方式。可以看到,Graph 图的构建非常灵活,我们可以自由地构建各种复杂的图结构。即使是没有与大模型交互的图,也可以通过 LangGraph 来构建。这对于处理传统任务也是非常有用的。
当然,LangGraph 中的图,还是要有大模型的加持,才能更好的体现他的强大之处。下一章节我们就着重去演练大模型加持下的 LangGraph。
在这里,不妨回顾一下LangChain 中的 Chain 是如何构建的,并与 Graph 做一下对比。可以看到,这两个框架都是着眼于将多个独立的功能模块组合进行调度、组合,形成复杂的智能体。只不过,LangChain 使用的是 Chain 的方式,而 LangGraph 是使用 Graph 的方式。或许这样能够更好的体会到,为什么 LangGraph是 LangChain 的一个子项目,而不是一个独立的框架了。
五、使用LangGraph构建多智能体工作流
上一章节,我们在没有大模型的加持下,全面演练了 LangGraph 的 Graph 图结构。这一章节,就结合大模型,来深入理解 LangGraph 如何通过 Graph 来构建复杂的大模型应用。
1、流式输出大模型调用结果
在介绍 Graph 的流式输出时,我们提到 LangGraph 的 Graph 流式输出有几种不同的模式,其中有一种 messages 模式,是用来监控大语言模型的 Token 记录的。这里我们就可以来测试下。
python
from config.load_key import load_key
from langchain_community.chat_models import ChatTongyi
# 构建阿里云百炼大模型客户端
llm = ChatTongyi(
model="qwen-plus",
api_key=load_key("DASHSCOPE_API_KEY"),
)
from langgraph.graph import StateGraph, MessagesState, START
from langgraph.checkpoint.memory import InMemorySaver
def call_model(state: MessagesState):
response = llm.invoke(state["messages"])
return {"messages": response}
builder = StateGraph(MessagesState)
builder.add_node(call_model)
builder.add_edge(START, "call_model")
graph = builder.compile()
for chunk in graph.stream(
{"messages": [{"role": "user", "content": "湖南的省会是哪里?"}]},
stream_mode="messages",
):
print(chunk)

2、大模型消息持久化
和之前介绍的 LangGraph 的 Agent 相似,Graph 图也支持构建消息的持久化功能。并且也通常支持通过 checkpointer 构建短期记忆,以 store 构建长期记忆。
这里的短期记忆和长期记忆,都是可以通过内存或者数据库进行持久化保存的。不过短期记忆更倾向于通过对消息的短期存储,实现多轮对话的效果;而长期记忆则倾向于对消息长期存储后支持的语义检索。
python
from config.load_key import load_key
from langchain_community.chat_models import ChatTongyi
# 构建阿里云百炼大模型客户端
llm = ChatTongyi(
model="qwen-plus",
api_key=load_key("DASHSCOPE_API_KEY"),
)
from langgraph.graph import StateGraph, MessagesState, START
from langgraph.checkpoint.memory import InMemorySaver
def call_model(state: MessagesState):
response = llm.invoke(state["messages"])
return {"messages": response}
builder = StateGraph(MessagesState)
builder.add_node(call_model)
builder.add_edge(START, "call_model")
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {
"configurable": {
"thread_id": "1"
}
}
for chunk in graph.stream(
{"messages": [{"role": "user", "content": "湖南的省会是哪里?"}]},
config,
stream_mode="values",
):
chunk["messages"][-1].pretty_print()
for chunk in graph.stream(
{"messages": [{"role": "user", "content": "湖北呢?"}]},
config,
stream_mode="values",
):
chunk["messages"][-1].pretty_print()

LangGraph 中围绕 CheckPoint 短期记忆,提供了非常丰富的补充功能。
3、Human-In-Loop人类干预
在 LangGraph 中也可以通过中断任务,等待确认的方式,来实现过程干预,这样能够更好的减少大语言模型的结果不稳定给任务带来的影响。
在具体实现人类干预时,需要注意以下几点:
- 必须指定一个
checkpointer短期记忆,否则无法保存任务状态 - 在执行 Graph 任务时,必须指定一个带有
thread_id的配置项,指定线程 ID。之后才能通过线程 ID,指定恢复线程。 - 在任务执行过程中,通过
interrupt()方法,中断任务,等待确认。 - 在人类确认之后,使用 Graph 提交一个
resume=True的 Command 指令,恢复任务,并继续进行。
这种实现方式,在之前介绍 LangGraph 构建单 Agent 时已经介绍过,不过,结合 Graph 的 State,在多个 Node 之间进行复杂控制,这样更能体现出人类监督的价值。
例如,下面的案例可以实现一种典型的人类确认:

python
# 构建一个带有Human-In-Loop的图
from operator import add
from langchain_core.messages import AnyMessage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.constants import START, END
from langgraph.graph import StateGraph
from config.load_key import load_key
from langchain_community.chat_models import ChatTongyi
# 构建阿里云百炼大模型客户端
llm = ChatTongyi(
model="qwen-plus",
api_key=load_key("DASHSCOPE_API_KEY"),
)
from typing import Literal, TypedDict, Annotated
from langgraph.types import interrupt, Command
class State(TypedDict):
messages: Annotated[list[AnyMessage], add]
def human_approval(state: State) -> Command[Literal["call_llm", END]]:
is_approved = interrupt(
{
"question": "是否同意调用大语言模型?"
}
)
if is_approved:
return Command(goto="call_llm")
else:
return Command(goto=END)
def call_llm(state: State):
response = llm.invoke(state["messages"])
return {"messages": [response]}
builder = StateGraph(State)
# Add the node to the graph in an appropriate location and connect it to the relevant nodes.
builder.add_node("human_approval", human_approval)
builder.add_node("call_llm", call_llm)
builder.add_edge(START, "human_approval")
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
from langchain_core.messages import HumanMessage
def safe_print(obj):
text = str(obj)
try:
print(text)
except UnicodeEncodeError:
print(text.encode('gbk', errors='replace').decode('gbk'))
# 提交任务,等待确认
thread_config = {"configurable": {"thread_id": 1}}
safe_print("=== 开始执行图 ===")
safe_print("触发 human_approval 节点,等待人工审批...")
for event in graph.stream({"messages": [HumanMessage("湖南的省会是哪里?")]}, thread_config, stream_mode="updates"):
for node, output in event.items():
safe_print(f"节点 [{node}] 输出: {output}")
safe_print("\n=== 人工审批通过,继续执行 ===")
for event in graph.stream(Command(resume=True), thread_config, stream_mode="updates"):
for node, output in event.items():
if output is None:
continue
safe_print(f"节点 [{node}] 输出: {output}")
if "messages" in output:
safe_print(f"LLM回复: {output['messages'][-1].content[:200]}")
safe_print("=== 执行完毕 ===")
跑通了,输出是:
- 触发图 → 进入 human_approval 节点,调用 interrupt 暂停,等待审批
- 审批通过 →
Command(resume=True)恢复,is_approved=True,进入call_llm- 调用 LLM → 通义千问回复:"湖南省的省会是长沙。"
总结文件作用:演示了 LangGraph 的 Human-In-Loop(人工介入)模式。核心就是
interrupt()--- 在图执行到一半时暂停,等待外部输入(人工审批),然后通过Command(resume=True)恢复执行。
注意:
- 任务中断和恢复,需要保持相同的
thread_id。通常应用当中都会单独生成一个随机的thread_id,保证唯一的同时,防止其他任务干扰 interrupt() 方法中断 任务的时间不能过长,过长了之后就无法恢复任务了。- 任务确认时,Command 中传递的 resume 可以是简单的 True 或 False,也可以是一个字典。通过字典可以进行更多的判断。
4、Time Travel时间回溯
由于大语言模型回答问题的不确定性,基于大语言模型构建的应用,也是充满不确定性的。而对于这种不确定性的系统,就有必要进行更精确的检查。当某一个步骤出现问题时,才能及时发现问题,并从发现问题的那个步骤进行重演。为此,LangGraph 提供了 Time Travel 的时间回溯功能,可以保存 Graph 的运行过程,并可以手动指定从 Graph 的某一个 Node 开始进行重演。
具体实现时,需要注意以下几点:
- 在运行 Graph 时,需要提供初始的输入消息
- 运行时,指定
thread_id线程ID。并且要基于这个线程 ID,再指定一个checkpoint检查点。执行后将在每一个 Node 执行后,生成一个check_point_id - 指定
thread_id和check_point_id,进行任务重演。重演前,可以选择更新 state,当然,如果没问题,也可以不指定。
python
# 构建一个图。图中两个步骤:第一步让大模型推荐一个有名的作家,第二步,让大模型用推荐的作家的风格写一个100字以内的笑话。
from typing import TypedDict
from typing_extensions import NotRequired
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.constants import START, END
from langgraph.graph import StateGraph
from config.load_key import load_key
from langchain_community.chat_models import ChatTongyi
# 构建阿里云百炼大模型客户端
llm = ChatTongyi(
model="qwen-plus",
api_key=load_key("DASHSCOPE_API_KEY"),
)
class State(TypedDict):
author: NotRequired[str]
joke: NotRequired[str]
def author_node(state: State):
prompt = "帮我推荐一位受人们欢迎的作家。只需要给出作家的名字即可。"
author = llm.invoke(prompt)
return {"author": author}
def joke_node(state: State):
prompt = f"用作家:{state['author']} 的风格,写一个100字以内的笑话"
joke = llm.invoke(prompt)
return {"joke": joke}
builder = StateGraph(State)
builder.add_node(author_node)
builder.add_node(joke_node)
builder.add_edge(START, "author_node")
builder.add_edge("author_node", "joke_node")
builder.add_edge("joke_node", END)
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
# 正常执⾏⼀个图
import uuid
config = {
"configurable": {
"thread_id": uuid.uuid4(),
}
}
state = graph.invoke({}, config)
print(state["author"])
print()
print(state["joke"])

python
print("----------")
states = list(graph.get_state_history(config))
for s in states:
print(s.values)
print(f"checkpoint_id: {s.config['configurable']['checkpoint_id']}")
print(f"next: {s.next}")
print()

python
# 选定某一个检查点,这里选择 author_node,让大模型重新推荐作家
selected_state = states[1]
print(selected_state.next)
print(selected_state.values)
print("----------------------")
# 为了后面的重演,更新state。可选步骤:
new_config = graph.update_state(selected_state.config, values={"author": "许嵩"})
print(new_config)
print("----------------------")
# 指定thread_id 和 checkpoint_id,进行重演
new_state = graph.invoke(None, new_config)
print(new_state["joke"]) # 打印许嵩风格的笑话

5、多智能体架构
可以看到,在 LangChain 体系中,LangChain 主要集成了和大语言模型交互的能力,而 LanguageGraph 主要实现了复杂的流程调度。将这两个能力集合起来,一个强大的多智能体构建就已经成型了。
接下来,我们就用 LangGraph 来实现一个非常典型的多智能体架构,作为一个完整的案例。
- 这个机器人可以通过一个 supervisor 节点,对用户的输入进行分类,然后根据分类结果,选择不同的 Agent 节点进行处理
- 接下来每个 Agent 节点,都可以选择不同的工具进行处理,最后将处理结果汇总,返回 supervisor 节点
- supervisor 节点再将结果返回给用户

在实现时,为了能够更综合的演练这么长时间的学习结果,我们对各个智能体的功能进行了一些设计,从而让这个小案例不再只是一个简单的 demo。
- 其他问题,只添加一个简单的响应结果
- 笑话助手,直接与大模型交互获得一个结果
- 对对联助手,从向量数据库中获取补充的资料,实现一个典型的RAG流程
- 路线规划助手,则需要调度外部的 MCP 服务,获取补充信息
这个案例,即作为 LangGraph 系列的总结演练,也作为一个典型的多智能体案例,强烈建议手动试试实现一个。在这个案例中,LangGraph 更多的帮助我们来梳理各个智能体之间如何协调。而具体实现时,可以更多的借鉴 LangChain 的能力。还有,不要忘了,LangGraph 还提供了很多开发过程中可以用到的工具,比如自定义流式输出、Time-Travel 时间重演等,都可以在这个案例中逐步尝试。
最终代码见视频。
总结
从 LangGraph 的整个演练过程可以看到,LangGraph 的核心是 Graph。Graph 其实是一个与大模型没有直接关联的,处理复杂任务的流程结构。LangGraph 或者说整个 LangChain 系列,其实是将传统的软件构建经验与大语言模型的能力进行结合,从而进一步打造出强大的智能体,解决更实际的复杂问题。这也进一步验证了,大语言模型未来的发展方向,一定是需要与传统应用相结合,这样才能更好的发挥大语言模型的价值。而这,或许是 LangChain 系列最核心的价值所在。
零帧起手,实现多智能体工作流
python
from operator import add
from os import write
from typing import TypedDict, Annotated
from anyio.lowlevel import checkpoint
from langchain_classic.chains.question_answering.map_reduce_prompt import messages
from langchain_community.chat_models import ChatTongyi
from langchain_core.messages import AnyMessage, HumanMessage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.config import get_stream_writer
from langgraph.constants import START, END
from langgraph.graph import StateGraph
from config.load_key import load_key
nodes = ["supervisor", "travel", "couplet", "joke", "other"]
# 构建阿里云百炼大模型客户端
llm = ChatTongyi(
model="qwen-plus",
api_key=load_key("DASHSCOPE_API_KEY"),
)
class State(TypedDict):
messages: Annotated[list[AnyMessage], add]
type: str
def other_node(state: State):
print(">>> other_node")
writer = get_stream_writer()
writer({"node": ">>> other_node"})
return {"messages": [HumanMessage(content="我暂时无法回答这个问题")], "type": "other"}
def supervisor_node(state: State):
print(">>> supervisor_node")
writer = get_stream_writer()
writer({"node": ">>> supervisor_node"})
# 根据用户的问题,对问题进行分类。分类结果保存到type中
prompt = """你是一个专业的客服助手,负责对用户的问题进行分类,并将任务分给其他Agent执行。如果用户
的问题是和旅游路线规划相关的,那就返回travel。
如果用户的问题是希望讲一个笑话,那就返回joke。
如果用户的问题是希望对一个对联,那就返回couplet。
如果是其他的问题,返回other。
除了这几个选项外,不要返回任何其他的内容。
"""
prompts = [
{"role": "system", "content": prompt},
{"role": "user", "content": state["messages"][0]}
]
# 如果已经有type属性了,表示问题已经交由其他节点处理完成了,就可以直接返回
if "type" in state:
writer({"supervisor_step": f"已获得{state['type']} 智能体处理结果"})
return {"type": END}
else:
response = llm.invoke(prompts)
typeRes = response.content
writer({"supervisor_step": f"问题分类结果:{typeRes} "})
if typeRes in nodes:
return {"type": typeRes}
else:
raise ValueError("type is not in (travel, joke, other, couplet)")
def travel_node(state: State):
print(">>> travel_node")
writer = get_stream_writer()
writer({"node": ">>> travel_node"})
return {"messages": [HumanMessage(content="travel_node")], "type": "travel"}
def joke_node(state: State):
print(">>> joke_node")
writer = get_stream_writer()
writer({"node": ">>> joke_node"})
return {"messages": [HumanMessage(content="joke_node")], "type": "joke"}
def couplet_node(state: State):
print(">>> couplet_node")
writer = get_stream_writer()
writer({"node": ">>> couplet_node"})
return {"messages": [HumanMessage(content="couplet_node")], "type": "couplet"}
# 条件路由
def routing_func(state: State):
if state["type"] == "travel":
return "travel_node"
elif state["type"] == "joke":
return "joke_node"
elif state["type"] == "couplet":
return "couplet_node"
elif state["type"] == END:
return END
else:
return "other_node"
# 构建图
builder = StateGraph(State)
# 添加节点
builder.add_node("supervisor_node", supervisor_node)
builder.add_node("travel_node", travel_node)
builder.add_node("joke_node", joke_node)
builder.add_node("couplet_node", couplet_node)
builder.add_node("other_node", other_node)
# 添加Edge
builder.add_edge(START, "supervisor_node")
builder.add_conditional_edges("supervisor_node", routing_func,
["travel_node", "joke_node", "couplet_node", "other_node", END])
builder.add_edge("travel_node", "supervisor_node")
builder.add_edge("joke_node", "supervisor_node")
builder.add_edge("couplet_node", "supervisor_node")
builder.add_edge("other_node", "supervisor_node")
# 构建Graph
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
# 执行任务的测试代码
if __name__ == "__main__":
config = {
"configurable": {
"thread_id": "1"
}
}
# for chunk in graph.stream({"messages": ["今天天气如何"]}, config, stream_mode="values"):
# print(chunk)
res = graph.invoke({"messages": ["今天天气如何"]}, config, stream_mode="values")
print(res["messages"][-1].content)

现在,就把框架完成了。未来,另外起一个服务,只需要拿到这个 graph,
python
import random
from Director import graph
config = {
"configurable": {
"thread_id": random.randint(1, 10000)
}
}
query = "请给我讲一个郭德纲的笑话"
res = graph.invoke({"messages": ["今天天气如何"]}, config, stream_mode="values")
print(res["messages"][-1].content)

补齐 joke_node:
python
def joke_node(state: State):
print(">>> joke_node")
writer = get_stream_writer()
writer({"node": ">>> joke_node"})
system_prompt = "你是一个笑话大师,根据用户的问题,写一个不超过100字的笑话。"
prompts = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": state["messages"][0]}
]
response = llm.invoke(prompts)
return {"messages": [HumanMessage(response.content)], "type": "joke"}

python
import asyncio
from operator import add
from typing import TypedDict, Annotated
from langchain_community.chat_models import ChatTongyi
from langchain_core.messages import AnyMessage, HumanMessage
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.config import get_stream_writer
from langgraph.constants import START, END
from langgraph.graph import StateGraph
from langgraph.prebuilt import create_react_agent
from config.load_key import load_key
nodes = ["supervisor", "travel", "couplet", "joke", "other"]
# 构建阿里云百炼大模型客户端
llm = ChatTongyi(
model="qwen-plus",
api_key=load_key("DASHSCOPE_API_KEY"),
)
class State(TypedDict):
messages: Annotated[list[AnyMessage], add]
type: str
def other_node(state: State):
print(">>> other_node")
writer = get_stream_writer()
writer({"node": ">>> other_node"})
return {"messages": [HumanMessage(content="我暂时无法回答这个问题")], "type": "other"}
def supervisor_node(state: State):
print(">>> supervisor_node")
writer = get_stream_writer()
writer({"node": ">>> supervisor_node"})
# 根据用户的问题,对问题进行分类。分类结果保存到type中
prompt = """你是一个专业的客服助手,负责对用户的问题进行分类,并将任务分给其他Agent执行。如果用户
的问题是和旅游路线规划相关的,那就返回travel。
如果用户的问题是希望讲一个笑话,那就返回joke。
如果用户的问题是希望对一个对联,那就返回couplet。
如果是其他的问题,返回other。
除了这几个选项外,不要返回任何其他的内容。
"""
prompts = [
{"role": "system", "content": prompt},
{"role": "user", "content": state["messages"][0]}
]
# 如果已经有type属性了,表示问题已经交由其他节点处理完成了,就可以直接返回
if "type" in state:
writer({"supervisor_step": f"已获得{state['type']} 智能体处理结果"})
return {"type": END}
else:
response = llm.invoke(prompts)
typeRes = response.content
writer({"supervisor_step": f"问题分类结果:{typeRes} "})
if typeRes in nodes:
return {"type": typeRes}
else:
raise ValueError("type is not in (travel, joke, other, couplet)")
def travel_node(state: State):
print(">>> travel_node")
writer = get_stream_writer()
writer({"node": ">>> travel_node"})
system_prompt = "你是一个专业的旅行规划助手,根据用户的问题,生成一个旅游路线规划。请用中文回答,并返回一个不超过100字的规划结果。"
prompts = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": state["messages"][0]}
]
# 高德地图的MCP配置信息
client = MultiServerMCPClient(
{
"amap-amap-sse": {
"url": "https://mcp.amap.com/sse?key=ef159aab7a695f4f647b35908731a575",
"transport": "sse"
},
# "amap-maps": {
# "command": "npx",
# "args": [
# "-y",
# "@amap/amap-maps-mcp-server"
# ],
# "env": {
# "AMAP_MAPS_API_KEY": "451ad40d0e39453600f2a305e31eabe4"
# },
# "transport":"stdio"
# }
}
)
tools = asyncio.run(client.get_tools())
agent = create_react_agent(
model=llm,
tools=tools
)
response = asyncio.run(agent.ainvoke({"messages": prompts}))
writer({"travel_result": response["messages"][-1].content})
return {"messages": [HumanMessage(content=response["messages"][-1].content)], "type": "travel"}
def joke_node(state: State):
print(">>> joke_node")
writer = get_stream_writer()
writer({"node": ">>> joke_node"})
system_prompt = "你是一个笑话大师,根据用户的问题,写一个不超过100字的笑话。"
prompts = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": state["messages"][0]}
]
response = llm.invoke(prompts)
# writer({"joke_result": response.content})
return {"messages": [HumanMessage(response.content)], "type": "joke"}
def couplet_node(state: State):
print(">>> couplet_node")
writer = get_stream_writer()
writer({"node": ">>> couplet_node"})
return {"messages": [HumanMessage(content="couplet_node")], "type": "couplet"}
# 条件路由
def routing_func(state: State):
if state["type"] == "travel":
return "travel_node"
elif state["type"] == "joke":
return "joke_node"
elif state["type"] == "couplet":
return "couplet_node"
elif state["type"] == END:
return END
else:
return "other_node"
# 构建图
builder = StateGraph(State)
# 添加节点
builder.add_node("supervisor_node", supervisor_node)
builder.add_node("travel_node", travel_node)
builder.add_node("joke_node", joke_node)
builder.add_node("couplet_node", couplet_node)
builder.add_node("other_node", other_node)
# 添加Edge
builder.add_edge(START, "supervisor_node")
builder.add_conditional_edges("supervisor_node", routing_func,
["travel_node", "joke_node", "couplet_node", "other_node", END])
builder.add_edge("travel_node", "supervisor_node")
builder.add_edge("joke_node", "supervisor_node")
builder.add_edge("couplet_node", "supervisor_node")
builder.add_edge("other_node", "supervisor_node")
# 构建Graph
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
# 执行任务的测试代码
if __name__ == "__main__":
config = {
"configurable": {
"thread_id": "1"
}
}
for chunk in graph.stream({"messages": ["帮我规划一条从成都光明城市小区到大熊猫基地的自驾路线"]}, config, stream_mode="values"):
print(chunk)
# res = graph.invoke({"messages": ["请你给我讲一个郭德纲的笑话"]}, config, stream_mode="values")
# print(res["messages"][-1].content)

对对联的:

python
# 将对联文本加载到向量数据库中
import os
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
import redis
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_redis import RedisConfig, RedisVectorStore
from config.load_key import load_key
if not os.environ.get("DASHSCOPE_API_KEY"):
os.environ["DASHSCOPE_API_KEY"] = load_key("DASHSCOPE_API_KEY")
embedding_model = DashScopeEmbeddings(model="text-embedding-v1")
# 保存向量数据库
redis_url = "redis://localhost:6380"
redis_client = redis.from_url(redis_url)
print(redis_client.ping()) # 测试连接,返回 True 则表示连接成功
config = RedisConfig(
index_name="couplet",
redis_url=redis_url
)
vector_store = RedisVectorStore(embedding_model, config=config)
lines = []
csv_path = Path(__file__).parent.parent / "resource" / "couplettest.csv"
with open(csv_path, "r", encoding="utf-8") as file:
for line in file:
print(line)
lines.append(line)
vector_store.add_texts(lines)

python
# 对联数据RAG
import os
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from langchain_core.prompts import ChatPromptTemplate
from langchain_redis import RedisConfig, RedisVectorStore
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.chat_models import ChatTongyi
from config.load_key import load_key
query = "帮我对个对联,上联是: 瑞雪兆丰年"
if not os.environ.get("DASHSCOPE_API_KEY"):
os.environ["DASHSCOPE_API_KEY"] = load_key("DASHSCOPE_API_KEY")
embedding_model = DashScopeEmbeddings(model="text-embedding-v1")
redis_url = "redis://localhost:6380"
from langchain_redis import RedisConfig, RedisVectorStore
config = RedisConfig(
index_name="couplet",
redis_url=redis_url
)
vector_store = RedisVectorStore(embedding_model, config=config)
samples = []
scored_results = vector_store.similarity_search_with_score(query, k=10)
for doc, score in scored_results:
# print(f"{doc.page_content} - {score}")
samples.append(doc.page_content)
prompt_template = ChatPromptTemplate.from_messages([
("system", """
你是一个专业的对联大师,你的任务是根据用户给出的上联,设计一个下联。
回答时,可以参考下面的参考对联。
参考对联:
{samples}
请用中文回答问题
"""),
("user", "{text}")
])
prompt = prompt_template.invoke({"samples": samples, "text": query})
print(prompt)
llm = ChatTongyi(
model="qwen-plus",
api_key=load_key("DASHSCOPE_API_KEY"),
)
print(llm.invoke(prompt))

这就是 RAG 的一个技术流程。
下面再把它加到 LangGraph 中。在 Director.py 里面加:
python
def couplet_node(state: State):
print(">>> couplet_node")
writer = get_stream_writer()
writer({"node": ">>> couplet_node"})
prompt_template = ChatPromptTemplate.from_messages([
("system", """
你是一个专业的对联大师,你的任务是根据用户给出的上联,设计一个下联。
回答时,可以参考下面的参考对联。
参考对联:
{samples}
请用中文回答问题
"""),
("user", "{text}")
])
query = state["messages"][0]
if not os.environ.get("DASHSCOPE_API_KEY"):
os.environ["DASHSCOPE_API_KEY"] = load_key("DASHSCOPE_API_KEY")
embedding_model = DashScopeEmbeddings(model="text-embedding-v1")
redis_url = "redis://localhost:6380"
config = RedisConfig(
index_name="couplet",
redis_url=redis_url
)
vector_store = RedisVectorStore(embedding_model, config=config)
samples = []
scored_results = vector_store.similarity_search_with_score(query, k=10)
for doc, score in scored_results:
# print(f"{doc.page_content} - {score}")
samples.append(doc.page_content)
prompt = prompt_template.invoke({"samples": samples, "text": query})
writer({"couplet_prompt": prompt})
response = llm.invoke(prompt)
writer({"couplet_result": response.content})
return {"messages": [HumanMessage(content=response.content)], "type": "couplet"}
python
# 执行任务的测试代码
if __name__ == "__main__":
config = {
"configurable": {
"thread_id": "1"
}
}
for chunk in graph.stream({"messages": ["给我对一个对联,上联是:金榜题名时"]}, config, stream_mode="values"):
print(chunk)

python
import random
from Director import graph
# config = {
# "configurable": {
# "thread_id": random.randint(1, 10000)
# }
# }
# query = "请给我讲一个郭德纲的笑话"
# res = graph.invoke({"messages": ["今天天气如何"]}, config, stream_mode="values")
# print(res["messages"][-1].content)
# grdio前端
import gradio as gr
def process_input(text): # 1个用法
config = {
"configurable": {
"thread_id": random.randint(1, 1000)
}
}
result = graph.invoke({"messages": [text]}, config)
return result["messages"][-1].content
with gr.Blocks() as demo:
gr.Markdown("# LangGraph Multi-Agent")
with gr.Row():
with gr.Column():
gr.Markdown("## 可以问路线规划,对对联,讲笑话,快来试试吧。")
inputs_text = gr.Textbox(label="问题*", placeholder="请输入你的问题", value="讲一个郭德纲的笑话")
btn_start = gr.Button("Start", variant="primary")
with gr.Column():
output_text = gr.Textbox(label="Output")
btn_start.click(process_input, inputs=[inputs_text], outputs=[output_text])
demo.launch()

低代码平台开发Agent
1、coze扣子 - AI办公助手一站式平台 - 扣子提供AI写作|PPT|表格|设计|播客|生图
2、difyDify 文档 - Dify Docs
Agent Skills
秋芝的手把手彻底学会 Agent Skills!【小白教程】_哔哩哔哩_bilibili
网页版教程:Agent Skills指南-秋芝2046
我自己实际做的时候,发现Google 的 AntiGravity 登陆不上,所以暂时不做。
Agent Skill从使用到原理,一次讲清
马克的技术工作坊的视频:Agent Skill 从使用到原理,一次讲清_哔哩哔哩_bilibili
2025年12月18日,Anthropic 正式将 Agent Skill 发布为开放标准。支持跨平台、跨产品复用。

这意味着 Agent Skill 已经超越了 Claude 单一产品的范畴,正在演变为 AI Agent 领域的一个通用的设计模式。
概念
什么是 Agent Skill?
用最通俗的话来讲,Agent Skill 其实就是一个大模型可以随时翻阅的说明文档。

基本用法
用「会议总结」这个实际的场景,来看看 Agent Skill 到底是怎么使用的。
这里使用 Claude Code 来演示:

在该 SKILL.md 文件中,输入以下内容:
markdown
---
name: 会议总结助手
description: 该技能用于根据会议录音总结内容
---
# 会议总结助手
## 总结规则
请将会议内容总结为以下几点:
- 参会人员
- 议题
- 决定
注意:每项都只能分别使用一句话来表述,不要分成多条。
## 示例
输入:
张三:那我们开始吧,今天主要是把下个月社区志愿活动的安排一次性定下来。
李四:我建议活动放在公园,人多也方便组织。
王五:可以,不过要提前申请场地,不然可能有风险。
赵六:场地申请我可以负责,这周内给大家结果。
孙七:人数最好先有个范围,方便准备物资。
张三:那就先按 50 人左右来估算吧。
李四:上次的手套还能用,但垃圾袋需要再买。
王五:预算要不要设个上限,避免超支。
张三:预算控制在 1000 以内,优先用现有物资。
孙七:时间我建议周六上午,天气也不会太热。
李四:九点集合应该比较合适。
赵六:我周三前把申请结果同步到群里。
张三:好,那报名截止时间定在周四晚上。
王五:周五可以统一分组和采购。
孙七:我来负责写报名文案和活动当天的合影安排。
张三:安全方面提醒大家带水,活动结束简单总结一下就行。
张三:那今天就到这,大家按分工推进。
输出:
- 参会人员:张三、李四、王五、赵六、孙七
- 议题:统一确定下个月社区志愿活动的地点、时间、人数、预算及分工安排。
- 决定:活动定在公园并于周六上午九点举行,按约 50 人规模和 1000 预算执行,由赵六负责场地申请、孙七负责宣传及合影,其余成员配合物资和分组。

现在 Agent Skill 就做好了。对,就是这么简单,就是一个说明文档。
下面,我们打开 Claude Code 来验货:



以上的流程图:

这就引出了 Agent Skill 的第一个核心机制:按需加载。虽然 Skill 的名字和描述是始终对模型可见的,但是具体的指令内容,只有在这个 Skill 被选中之后,才会被加载进来给模型看。这样便可以节省很多的 Token 了。
高级用法(References)
按需中的按需加载:

markdown
# 集团财务手册
本手册详细规定了公司各部门在日常办公、差旅及商务活动中的支出限额与审批流程。
## 第一章:办公设备采购(IT Assets)
1. **更换周期**:笔记本电脑、显示器等固定资产的最低使用年限为 3 年。
2. **采购限额**:
- 标准办公电脑:单价不得超过 10,000 元。
- 高性能工作站:单价 10,000 - 20,000 元,需部门总监(Director)审批。
- 特殊定制设备:单价超过 20,000 元,必须由 IT 总监特批,并提交 CFO 最终签字。
3. **招标要求**:单笔采购总额超过 50,000 元时,必须启动至少三方参与的公开招标流程。
## 第二章:国内差旅标准(Domestic Travel)
1. **住宿补贴(按城市等级)**:
- 一线城市(北京、上海、广州、深圳):800 元/晚。
- 新一线及二线城市:500 元/晚。
- 其他城市:350 元/晚。
2. **交通工具**:
- 飞行时长 4 小时以内仅限经济舱。
- 高铁限二等座(部门副总及以上级别可选一等座)。
## 第三章:商务招待与餐饮(Entertainment)
1. **招待标准**:
- 商务正餐:人均限额 300 元。若超过 300 元/人(如上海、香港等高消费地区最高可至 500 元/人),
需附完整参会名单并提交业务副总裁(VP)特批。
2. **随访要求**:内部陪同人员人数不得超过外部客人数。
## 第四章:日常零星报销
1. **自主额度**:单笔 500 元以下的办公杂费支出可由员工自主报销。
2. **主管审批**:500 元至 5,000 元的支出由部门直接主管在系统内审批。
## 第五章:市场活动与公关
1. **预算申报**:所有涉及品牌推广、市场活动的预算需提前 14 天提交 OA 流程申报。
2. **礼品采购**:单份赠礼价值上限为 300 元。
---
*注:以上所有金额单位均为人民币(CNY)。违反以上限额且未获得特批的申请,财务部将予以退回。*

这就是 Reference 的核心逻辑了。
在 Agent Skill 体系里面,集团财务手册.md 这个文件就是个典型的 Reference。它是条件触发的。
在刚才的例子里,只有当 Claude Code 读取完 SKILL.md 文件,判断出需要查账时才会去加载这个文件。
反过来说,若这是一个与钱无关的"技术复盘会",那么这个财务文件就只会躺在硬盘里面,绝不会占用哪怕一个 Token 的上下文。
高级用法(Script)
下面来讲如何让 Agent Skill 跑代码。毕竟查资料只是第一步,能直接动手运行代码帮我们把活干了,这才是真正的自动化。
这就用到了 Agent Skill 的另一大能力:Script。

python
import sys
import time
def upload_summary(content):
print("\n[System] 启动上传程序 ... ")
time.sleep(0.5)
print("[System] 正在连接公司内部服务器 (https://api.internal.wiki) ... ")
time.sleep(1.2)
# 模拟数据处理
print(f"[System] 正在上传总结内容 (字符数: {len(content)}) ... ")
time.sleep(1.0)
print("------------------------------------------------------------------------------------")
print("✅ 上传成功!")
print(f"📄 文档已保存至: /meetings/2024/summary_{int(time.time())}.md")
print("🔗 预览链接: https://wiki.internal.com/view/99281")
print("------------------------------------------------------------------------------------")
if __name__ == "__main__":
# 获取 Claude 传入的总结文本
if len(sys.argv) > 1:
summary_text = sys.argv[1]
upload_summary(summary_text)
else:
print("❌ 错误:未接收到总结内容。")




可以看到,我使用的会议内容跟"钱"其实没什么关系,所以 Claude Code 也并没有去读取 "集团财务手册.md" 那个文件,结果中也没有财务提醒相关的内容。
这正好印证了前面所说的观点,Reference 是按需加载的,如果用户没有提到与 Reference 相关的内容,那 Claude Code 是不会去读取它的。
这样就达到了节省上下文 token 的目的。
回到代码执行部分,从上图中可看到,Claude Code 只是申请执行 upload.py 这个文件,而没有读取这个文件。
所以,Agent Skill 里面的代码只会被执行,不会被读取。这就意味着,哪怕你的脚本写了一万行复杂的业务逻辑,它消耗的模型上下文也几乎是零。
Claude Code 只关心脚本的运行方法和运行结果,至于这个脚本的内容,它是毫不在意的。
所以,虽然 Reference 和 Script 都属于 Agent Skill 的高级功能,但是它们对于模型上下文的影响是截然不同的。
- Reference 是读:会把内容加载到上下文里面,要消耗 Token。
- Script 是跑:只会被执行,不会占用模型的上下文。
渐进式披露机制
Agent 的设计其实是一个精密的渐进式披露结构。结构里面一共有 3 层,每一层的加载机制都不太一样。
- 第一层:元数据层(Metadata),这里有所有 Agent Skill 的名称和描述,它们是始终加载的,相当于大模型里面的目录。大模型每次回答前都会看下这一层的信息,然后决定用户的问题是否与某个 Agent Skill 相匹配。
- 第二层:指令层(Instruction):对应 SKILL.md 文件里面,除了名称和描述之外其余的部分。只有当大模型发现用户的问题与某个 Agent Skill 相匹配的时候,它才会去加载这一层的内容。所以我们称这一层为按需加载。
- 第三层:资源曾(Resource):最深的一层,包含 Reference 和 Script 两方面的内容。
- 按照官网最新的规范,应该还有一个组成部分叫做 Asset,不过它跟 Reference 的定义似乎有部分重叠,因此我们这里先忽略它。

Agent Skill vs MCP
到这里,就会有感觉,Agent Skill 好像是和 MCP 有点像啊?本质上都是让模型去连接和操作外部世界。
既然功能重叠,那我们到底用哪个呢?
Anthropic 官方写过一篇具体的文章来解释:
核心观点就一句话:MCP 给大模型供给数据,Skill 教大模型如何处理这些数据。
MCP 本质上是一个独立运行的程序,而 Agent Skill 本质上是一段说明文档。
它们的本质不同,决定了适合的场景也是不同的:Agent Skill 适合跑一些轻量的脚本,处理简单的逻辑;在代码执行方面,Agent Skill 的安全性和稳定性都不及 MCP。
所以还是要根据场景选择合适的工具。
甚至在很多的场景下,我们需要把 MCP 和 Agent Skill 结合起来使用。以便尽可能满足我们的需求。

7分钟速通Agent Skill是什么?
7分钟速通Agent Skills是什么?跟MCP|Workflow|Command|Prompt有什么关系?_哔哩哔哩_bilibili
跟上个视频大差不差。
Skill vs Workflow
很多任务其实可以拆解成好几个步骤。比如做视频,可以分为"找选题"→"写文案"→"做分镜"等几个步骤。

为了解决这类流程化需求,不少大佬开源了一些低代码工具。比如 n8n。通过拖拉拽的方式快速构建一条流水线。
这种通过"规则配置",把多个步骤进行编排和调度的流程,就叫 Workflow。
Skills 本质上也是做"逻辑编排"。但跟 Workflow 不同的是:
Workflow 的流程结构在设计阶段就确定好了。而 Skills 的执行流程则由大模型驱动,灵活性相对更高。两者最终都能做到类似的功能。

skills 可以简单理解为"大模型驱动的 workflow"。
10 分钟,从0到「会写Skill」科普
10 分钟,从0到「会写Skill」科普_哔哩哔哩_bilibili
抓包ClaudeCode窥探Skill的实现原理
抓包ClaudeCode窥探Skill的实现原理_哔哩哔哩_bilibili
抓包ClaudeCode窥探Skill的渐进式加载的过程
抓包ClaudeCode窥探Skill的渐进式加载的过程_哔哩哔哩_bilibili
Agent核心技术
一口气拆穿Skill/MCP/RAG/Agent/OpenClaw底层逻辑
【闪客】一口气拆穿Skill/MCP/RAG/Agent/OpenClaw底层逻辑_哔哩哔哩_bilibili

他认为:SKILL 兼顾了灵活性和稳定性,后面(他认为)会逐渐淘汰掉 MCP 和 workflow。
- MCP:常用工具,他认为会直接内化到 Agent 的主程序中,或者在未来的基础 SKILL 包中存在,比较鸡肋
- Workflow:既不如 Langchain 一样适合程序员,也不如 SKILL 一样适合我们普通人,也是比较鸡肋的存在
- SKILL:他认为也是个中间产物,未来一定会有更方便的形式出现,让所有人都可以很符合直觉地无脑使用
从 LLM 到 Agent Skill,一期视频带你打通底层逻辑!
「马克的技术工作坊」的视频:从 LLM 到 Agent Skill,一期视频带你打通底层逻辑!_哔哩哔哩_bilibili
LLM
Large Language Model,大语言模型,简称大模型。

现在所有的大模型都是基于 Transformer 架构训练出来的。
Transformer 架构最早是由 Google 团队在 2017 年提出来的,对应的论文名是:《Attention Is All You Need》。

戏剧性的是:Google 虽然发明了火种,但真正把它点燃,并且引爆全世界的却是 OpenAI。
GPT 系列就是今天 AI 浪潮的绝对鼻祖。
以上是大模型的由来。那么大模型是如何工作的呢?
其实非常朴素,它本质上就是一个文字接龙游戏。比如:
-
用户问大模型:马克 视频怎么样
-
模型接收到这句话之后,经过内部的运算,去预测下一个概率最高的词,比如"特别"
-
模型吐出"特别"这个词之后,并不会停下来。它会再把"特别"重新追加到你刚才的那个输入后面

-
然后接着预测,吐出下一个字,比如是"得"。

-
再把"得"塞回去,再预测下一个词,比如说是"棒"。
-
然后再把"棒"这个字也塞回到输入里面。
-
这时候,大模型发现,它要说的话已经全部说完了。此时它就会输出一个特殊结束标示符✅。然后整个回答到这就算彻底结束了。
以上是大模型最底层的生成原理。理解了这一点,就明白了大模型为啥要一个词一个词地输出答案。因为它就是这么运作的。
Token
刚才说了用户提交问题给大模型后,大模型每次都会吐出一个词。但这其实是为方便理解而简化的一个链路。
大模型本质上是一个庞大的数学函数,里面跑的全是矩阵运算。它接收的是数字,输出的也是数字,压根就不认识人类写的文字。所以,在人类和大模型之间,必须有一个中间人来做翻译。这个中间人叫 Tokenizer,它负责的是编码和解码两件事情。

所以,Token 才是大模型处理文本的「最基本单位」。大模型一个 Token、一个 Token 的接收输入,然后再一个 Token、一个 Token 的输出结果。
注意:Token ≠ 词,Token 和 词并不是一一对应的关系。刚才那个例子只是恰好而已。
Open AI 提供了一个把文本转换为 Token 的页面:OpenAI Platform
反正记住:词和 Token 并没有啥明确的一对一的关系。可以把 Token 理解为模型自己学会的一套文本切分规则,切出来的每一块,就是它一次能够处理的最小单位。
平均来讲:
Context
我们平时和大模型聊天,它好像能记住你之前说过的话。问题是:大模型本质上只是一个数学函数,你给它输入,它就给你输出。它并不像人一样真的有记忆。
那它是怎么记住之前的聊天内容的呢?

这就引出了 Context 的概念,中文叫:上下文。它代表大模型每次处理任务时所接收到的信息总和。

又有一个新问题:这个 Context 能有多大?能塞进多少 Token 呢?这就引出了 Context Window 这个概念,翻译过来就叫做上下文窗口。
它代表了 Context 能容纳的最大「Token 数量」。

下面思考一个问题:假如你有一个上千页的公司产品手册,你希望大模型根据这个产品手册来回答用户的各种疑问,那这要怎么实现呢?你要把这个手册的全部内容跟着用户问题一起扔给大模型吗?这其实不是一个很好的解决方案,因为这个产品手册太长了,即使模型的 Context Window不被撑爆,你的成本也无法控制。那这该怎么办呢?
这就需要一个叫做 RAG 的技术了。可以从产品手册中抽取用户问题最为匹配的几个片段,然后只把这几个片段发给大模型,让大模型只根据这几个片段来回答用户的问题。这样大模型接到的就不是一整本书了,可能只是几段话。这样就不受 Context Window 大小限制了,成本也会低很多。(具体 RAG 之前也是学了的)

Prompt
Prompt,中文为提示词,它是大模型接收的具体问题或指令,比如你去向大模型提需求:帮我写一首诗。这句话呢,就是 Prompt。对,不要把 Prompt 想成特别复杂高端的东西,它只不过就是给大模型的一个问题或者是指令而已。
接到了这个输入之后,大模型才会开始运转,然后才会给你一个对应答案。但这里面会有个问题,如果你只是简单地说帮我写一首诗,大模型可能会给你写古诗,就像屏幕上给你展示的这样,但也可能给你写现代诗,甚至可能来一首打油诗,为什么呢?因为你的 Prompt 太模糊了,它不知道你具体想要什么,所以 Prompt怎么写直接决定了大模型的输出质量。一个好的 Prompt 应该是清晰的,具体的,明确的。
比如你可以这样写:请帮我写一首五言绝句,主题是秋天的落叶,风格要悲凉一点,这样一来,大模型就清楚多了,它生成的内容也就更符合你的预期。这就是为什么有个专门的领域叫做 Prompt Engineering,也就是提示词工程,说白了就是研究怎么把话说清楚,让大模型更精准地理解你的意图。当然,这个领域虽然曾经比较火,但现在还在提它的人其实寥寥无几。
还没完,有些时候,我们不仅要告诉大模型它要处理的具体任务,还要告诉它人设和做事规则。也就是告诉大模型它是谁,它应该按照什么规则做事。
所以这就引出了两种不同的 Prompt,说明具体任务的是 User Prompt,说明人设和做事规则的是 System Prompt(系统提示词),它是开发者在后台配置的。

Tool
大模型的弱点:它无法感知外界环境。

假设你问大模型:今天上海的天气怎么样?它可能会说,抱歉,我无法获取实时天气信息,我的知识库即使到 XX 年 XX 月,无法提供当前的天气数据。
为什么呢?因为大模型只是个文字接龙游戏,它的能力是根据训练数据来预测下一个词,但它真的没有办法去查天气预报网站,拿到实时的天气数据,这该怎么办呢?这就需要 Tool 了,Tool 翻译成中文就是「工具」。
「工具」这个词不太好理解,我们再换一个词,函数。对没错,tool 本质上,就是一个函数,你给它输入,它就给你输出。
比如一个天气查询工具,它的输入呢,可能会包含两个参数,分别是城市和日期,传入后,它内部一通操作,比如说它可能会去调用气象局的接口,但不管怎么样,最后它都会给你一个输出,告诉你对应的天气信息。有了它,大模型就可以回答天气相关的问题了。
让我们来看一下从用户提问到大模型回答的完整流程。

总结下:Tool 的本质是给大模型提供一套它可以调用的外部能力,让大模型能够感知和影响外部环境。

MCP
刚才讲了使用工具的全流程,但其中有 2 个问题。
① 平台要把工具列表传给模型
② 能调用工具

要做到这些,就首先得把工具接入到平台里面。这样,平台才知道可用的工具列表以及每个工具的用途、参数和调用方法等。
问题来了,这套接入的规范,每个平台都不一样:

MCP 就是这个统一的接入规范。把它理解成一套统一的工具接入标准即可。
有了 MCP 后,工具的开发者只需要按照 MCP 的规范开发一次工具,这个工具就可以被所有支持 MCP 的平台使用了。
这就像是所有的手机都用 Type-C 接口一样。有了统一的标准,大家都会方便很多。
这就是 MCP 的由来及作用。更深入具体的前面也学过。
Agent
现在,就是这样了:

但还差点东西。比如我们来尝试让大模型解决一个更有难度的问题:今天我这里的天气怎么样?如果下雨的话,帮我查一下附近有没有卖雨伞的店。然后呢,我们假设有这些工具可用:
- 定位工具,它负责查询用户所在地区的经纬度。
- 天气工具,它是用来根据经纬度查询天气信息。
- 店铺工具,它是通过经纬度来查询附近的店铺。

这就不是一个简单的调用流程了,在这个步骤中,大模型需要一步一步思考当前的情况,并决定下一步做什么。
从某种程度上来说,大模型已经有了一定的自主规划能力。
我们称这种能够自主规划,自主调用工具 ,直至完成用户任务的系统为 Agent。

Agent Skill
具体就看 Agent Skill 那节吧。
总结

理解了这些概念,你就能看懂 AI 圈子里面的各种新产品、新技术了。
无论是 Claude Code、Codex、Cowork,还是 OpenClaw,它们本质上都是在这个框架下运作的。
CLI
为什么越来越多的人抛弃 MCP,转向 CLI?
MCP 曾经被誉为 Agent 的万能接口,连接一切工具的标准。
但最近越来越多的顶级开发者开始悄悄抛弃它,转而用一种更原始的方式来解决问题------就是直接调用命令行程序。

这些命令有一个共同点,它们一般都是在**命令行界面(Command Line Interface,CLI)**里执行的。

那么问题来了:MCP 明明是专门为大模型设计的工具接口标准,为什么现在反而被古老的 CLI 工具抢饭碗呢?
CLI 到底有啥惊为天人的优势,而 MCP 又有啥不为人知的问题呢?
CLI 工具的优势,他总结了下,主要有 2 点:Token 消耗小,以及执行效率高。
CLI 工具的优势(Token 消耗小)
从反面来看,就意味着 MCP 工具的 Token 消耗大。尤其是 MCP 的元信息,包括名称、描述、入参格式等,这些都会传到大模型的上下文里面,从而消耗大量 Token。

CLI 流程里面,只需要传一个 bash 工具给大模型就行。

在 CLI 这个流程里面,我们只需要传一个 bash 工具给大模型就行,而这个工具的说明就十几行,这跟我们之前看到的那种上万行的 MCP 工具说明相比,Token 的消耗量几乎可以忽略不计。
像 gh、git、grep 这种常见的 CLI 程序,大模型在训练阶段就已经见识过大量的用法了。所以大模型知道。

CLI 工具的优势(执行效率高)
来看一个具体的场景:
假设你是一名摄影师,刚拍完一批素材。文件夹里面有 10 张单反照片。现在你的任务是:把所有的横版照片(宽 > 高)的照片找出来。找出来后,加上你的专属水印,再上传到你的服务器上,用作网页展示。

在 MCP 模式下,会发生什么?

大模型是整个链路的调度中心。所有操作都必须要经过它,少一步都不行。这也就意味着,流程里面的每一步,都要等大模型响应一次,整体效率自然就卡在这里了。
再来看下 CLI 模式下是怎么做的?

命令发出去后,这三步全部在本地自动跑完,不需要大模型的参与。
等所有操作执行完毕,结果才回到大模型这里。

可见,CLI 的链路要比 MCP 短得多,效率自然也会高很多。
为什么 CLI 可以做到这一点呢?因为 CLI 的程序可以随意组合。

你可能会想:那我把读取目录、读取图片信息、加水印、上传,这几个工具做成一个 MCP 工具,不就行了吗?
一口气把所有的事情全部干完,这样整个流程调用一次 MCP 工具就行了,那是不是也可以达到跟 CLI 工具一样的效果呢?理论上确实可以,但问题是需求稍稍一变,这个工具就不够用了。

而 CLI 天生就是组合式的。换个需求,调整几个参数,重新拼一下,几秒钟就搞定了。这种灵活性,是 MCP 工具难以复刻的。
MCP工具的优势(更可控)
MCP 能够成为行业标准,那也是有其无可替代的优势的。
他总结了下,MCP 工具的优势有 2 点,更可控、更安全。

更可控,说白了,就是 MCP 工具更不容易出错。

虽然大模型非常擅长生成 CLI 命令。但现实是:命令越复杂,出错的概率就越高。而且这类错误往往很隐蔽。人类在审查的时候很难一眼看出来。
MCP 则是老老实实的躺在双引号里面,不会有这个问题。

MCP工具的优势(更安全)
CLI 命令的灵活性是一把双刃剑。它什么都能做,也就意味着它什么都能搞砸。
大模型生成的命令里面,万一夹带了一个 rm -rf 之类的操作,本地文件可能就误删了。有可能大模型是好心的,它只是想删除临时文件而已。但是它犯错了,我们可避免不了,而你可能执行之后才后知后觉,这时候就已经晚了。
在本地环境,这个风险你或许还能接受,但若是在云端环境呢?
很多云端的自动化服务,比如 Make.com,它允许你在工作流里面接入 MCP 服务,但它绝对不会让你直接执行一条 bash 命令。原因很简单,它是一个共享环境,你在上面跑的每一步操作,都有可能影响到其他用户。一旦放开 CLI 权限,一条失控的命令就有可能把整个服务器,甚至整个集群搞崩溃。
当然现在也有一些云端产品支持 CLI,但它们都是做了严格限制的,本质上还是通过沙箱和权限控制把风险锁在一个可控范围内。这个成本就很高了,而且安全性也很难达到 MCP 的级别。
所以,在安全性要求很高的场景,MCP 这种受限的设计,反而成了一种优势。它只能做工具设计者允许它做的事情,不多也不少。

未来属于谁?
他的判断是:**CLI 工具的比重会越来越大,而 MCP 的比重会逐渐缩小。**对于大部分场景和大部分人来说,CLI 就是一个更快、更便宜、更直接的选择。
CLI 会越来越多的走向个人,而 MCP 会留在企业和云端。

大模型微调(了解下)
什么是LoRA 大模型微调是怎么回事
什么是LoRA 大模型微调是怎么回事_哔哩哔哩_bilibili
基于LoRA+Fast API微调DeepSeek大模型
LoRA 算法论文解读 & 开发人员如何微调大模型并暴露可调用接口_哔哩哔哩_bilibili
大模型微调实践入门
P1~P9
(超爽中英!) 2024公认最好的【LLM微调大模型】系列教程!附课件代码 Fine-tuning Large Language Models_哔哩哔哩_bilibili









