1. 为什么我们需要手动实现DeepSeek R1的工具调用?

最近在捣鼓DeepSeek R1的时候,我发现了一个挺让人头疼的问题:这模型本身不支持原生的工具调用。官方在GitHub上倒是挺坦诚,说下一个大版本会支持,但具体啥时候能等到,谁也说不好。这就好比你买了一辆性能超强的跑车,结果发现它没有方向盘——动力是有了,但你想往哪儿开,完全使不上劲。

对于咱们做AI应用开发的来说,工具调用可不是什么锦上添花的功能,而是刚需。你想啊,一个AI模型再聪明,它知道的也只是训练数据截止日期之前的世界。让它查今天的天气、帮你订张机票、或者从数据库里拉点实时数据,它自己根本办不到。这时候,工具调用就派上用场了:让AI学会“使用工具”,把复杂任务拆解成“思考-行动-观察”的循环,最终调用外部API或者执行一段代码来解决问题。

我试过直接问DeepSeek R1:“今天北京天气怎么样?”它可能会根据历史数据给你编一个答案,或者干脆告诉你它不知道实时信息。这显然不是我们想要的。我们需要的是它能意识到“我需要调用一个天气查询工具”,然后生成规范的调用指令,我们收到指令后去执行真正的API调用,再把结果喂回给它,让它生成最终的人类可读答案。这个过程,就是ReAct(Reasoning + Acting)思想的核心。

所以,与其干等官方更新,不如咱们自己动手,丰衣足食。用Java,结合Spring AI这个越来越火的框架,再套上ReAct的“思考模板”,完全可以在DeepSeek R1上实现一套手工打造的工具调用流程。听起来有点复杂?别怕,我一步步带你走一遍,踩过的坑都给你标出来,保证你能跟着做出来。

2. 理解ReAct:让AI学会“动脑”和“动手”

在开始敲代码之前,咱们得先把ReAct这个思想吃透。你可以把它想象成教一个特别聪明但缺乏实践能力的孩子解决问题。比如,孩子想从高高的柜子上拿饼干。他不会直接跳起来抓(那不可能),而是会先“思考”:我需要一个垫脚的东西。然后“行动”:去搬一把椅子。接着“观察”:椅子够不够高?如果不够,再进入下一轮“思考”:也许还需要再垫几本书。如此循环,直到最终“行动”:站上去拿到饼干。

把这个逻辑搬到AI对话里,就形成了ReAct的标准流程。AI的每次回复不再是一锤子买卖,而是一个循环中的一步。它输出的内容被严格格式化成几个部分:Thought(我目前的分析和下一步计划)、Action(我要调用哪个工具,参数是什么)、Observation(上次工具调用的结果)。只有当它认为信息足够时,才会输出 Final Answer

为什么非得用这个格式?因为DeepSeek R1不支持结构化输出(比如强制它返回一个固定的JSON字段)。我们只能通过精心设计的提示词(Prompt),去“引导”它按照我们规定的格式说话。这个提示词就像一份详细的“剧本”和“行为规范”,告诉AI:“你说话必须按这个套路来,先报告思考,再报告行动,行动必须包在特定的标记里。”

我最初尝试时,提示词写得比较松散,结果AI经常“演砸”——要么忘了输出Action标记,要么把思考和答案混在一起说,导致我的程序根本无法解析下一步该干什么。后来我把提示词打磨得非常严格,几乎到了“强迫症”的地步,效果才稳定下来。这份中文版的提示词模板,是我调试了很多次后的成果,你可以直接拿去用:

尽可能帮助用户,并提供准确的回答。
{{instruction}}
你可以使用以下工具:
{{tools}}
通过使用 JSON 对象来指定工具,提供 action(工具名称)和 action_input(工具输入)键。
有效的 “action” 值有:“Final Answer” 或 {{tool_names}}。
每个 $JSON_BLOB 只能指定一个动作,格式如下:
###
{
  "action": $TOOL_NAME,
  "action_input": $ACTION_INPUT
}
###
遵循以下格式:
问题(Question):要回答的输入问题
思考(Thought):考虑之前和接下来的步骤
行动(Action):
### $JSON_BLOB ###
观察(Observation):行动结果
… (重复“思考/行动/观察”多次,直至完成任务)
思考(Thought):我知道如何回答了
行动(Action):
###
{
  "action": "Final Answer",
  "action_input": "Final response to human"
}
###
开始!请务必始终以有效的 JSON 对象形式提供单个Action。
如果需要工具就使用工具。
如果适合,请直接回应用户。
格式为:###$JSON_BLOB###然后 Observation:.

这里有几个关键点你一定要注意:

  1. {{instruction}}:这是你的系统指令,比如“你是一个有帮助的助手”。
  2. {{tools}}:这里要替换成你定义的所有工具的JSON Schema描述。AI需要知道每个工具叫什么、能干什么、需要什么参数。这部分描述越清晰,AI调用得越准。
  3. {{tool_names}}:就是所有工具名称的列表,用逗号隔开,比如 "查询天气, 搜索新闻, 计算器"。这用来明确告诉AI有哪些选项。
  4. 严格的格式和分隔符:我用了 ### 作为JSON块的开始和结束标记。这个标记要足够独特,避免和AI正常输出的内容混淆。同时,整个“思考-行动-观察”的循环结构必须反复强调,直到AI形成“肌肉记忆”。

3. 搭建Spring AI项目环境与核心依赖

理论讲完了,咱们动手搭环境。我选择Spring AI,是因为它和Spring Boot生态无缝集成,配置起来比直接用原生API省心很多,尤其是在管理对话历史、处理消息流这些方面。

首先,用你喜欢的IDE(比如IntelliJ IDEA)或者Spring Initializr创建一个新的Spring Boot项目。我这里假设你用Maven,Gradle的配置也类似。关键的依赖就下面这几个:

<dependencies>
    <!-- Spring Boot Web (如果需要提供HTTP接口的话) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring AI 核心 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-core</artifactId>
        <version>0.8.1</version> <!-- 请使用最新稳定版 -->
    </dependency>

    <!-- Spring AI 对DeepSeek的支持 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-deepseek</artifactId>
        <version>0.8.1</version>
    </dependency>

    <!-- 工具类,比如JSON处理 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-json</artifactId>
    </dependency>

    <!-- 测试 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

注意:Spring AI的版本迭代比较快,你一定要去官方文档查一下当前推荐的稳定版本。我写这篇文章时0.8.1是稳定的,但等你看到的时候可能已经更新了。

依赖加好后,在 application.ymlapplication.properties 里配置你的DeepSeek API密钥和基础URL。DeepSeek的API目前是免费的,但你需要去官网申请一个API Key。

spring:
  ai:
    deepseek:
      base-url: https://api.deepseek.com
      api-key: ${DEEPSEEK_API_KEY:你的API密钥}
      chat:
        options:
          model: deepseek-r1
          temperature: 0.1 # 温度调低点,让输出更稳定,更遵守格式

这里我把 temperature 调到了0.1,甚至0。在工具调用场景下,我们不需要AI有多大的“创意”,恰恰相反,我们需要它严格地、可预测地遵守我们规定的输出格式。低温度值能大大减少它“胡言乱语”、破坏格式的概率。

环境搭好,启动一下项目,确保没有报错。接下来,我们就要进入最核心的部分:定义工具,并实现那个驱动AI“思考-行动”循环的引擎。

4. 定义与封装AI可用的工具

工具是AI的“手”和“眼”。在Spring AI里,定义一个工具特别简单,就是一个实现了 Tool 接口的普通Spring Bean。这个接口主要就是一个 call 方法,接收一个参数(通常是字符串形式的JSON),返回一个字符串结果。

我们来定义两个最常用的工具当例子:一个天气查询,一个简单计算器。

import org.springframework.ai.tool.Tool;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Map;

@Component
public class WeatherTool implements Tool {

    @Override
    public String getName() {
        return "get_weather";
    }

    @Override
    public String getDescription() {
        return "根据城市名称查询该城市当前的天气情况。";
    }

    @Override
    public Map<String, String> getParameters() {
        // 定义输入参数的Schema,这里用简单的Map,也可以用JSON Schema对象
        return Map.of(
            "city", "要查询天气的城市名称,例如:北京、上海"
        );
    }

    @Override
    public String call(String input) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            ObjectNode node = (ObjectNode) mapper.readTree(input);
            String city = node.get("city").asText();

            // 这里应该是调用真实天气API,比如和风、OpenWeatherMap等
            // 为了演示,我们模拟一个返回
            // 实战中,请在这里发起HTTP请求并解析结果
            String mockResult = String.format("城市【%s】的天气情况:晴,温度25℃,湿度60%,东南风2级。", city);
            return mockResult;

        } catch (Exception e) {
            return "查询天气时发生错误:" + e.getMessage();
        }
    }
}
@Component
public class CalculatorTool implements Tool {

    @Override
    public String getName() {
        return "calculator";
    }

    @Override
    public String getDescription() {
        return "执行简单的数学计算,支持加(+)、减(-)、乘(*)、除(/)。";
    }

    @Override
    public Map<String, String> getParameters() {
        return Map.of(
            "expression", "数学表达式,例如:'3 + 5 * 2'"
        );
    }

    @Override
    public String call(String input) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            ObjectNode node = (ObjectNode) mapper.readTree(input);
            String expression = node.get("expression").asText().replaceAll("\\s+", "");

            // 警告:这里使用ScriptEngine仅作最简单演示,生产环境请使用更安全的表达式求值库!
            javax.script.ScriptEngineManager mgr = new javax.script.ScriptEngineManager();
            javax.script.ScriptEngine engine = mgr.getEngineByName("JavaScript");
            Object result = engine.eval(expression);

            return String.format("表达式 `%s` 的计算结果是:%s", expression, result.toString());
        } catch (Exception e) {
            return "计算表达式时发生错误:" + e.getMessage();
        }
    }
}

定义好工具后,Spring AI会自动发现它们。但是,我们手动实现的ReAct引擎需要自己来管理和调用这些工具。所以,我们还需要一个 ToolManager 来集中管理所有的工具,方便根据名称查找和执行。

import org.springframework.ai.tool.Tool;
import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class ToolManager {
    private final List<Tool> tools;
    private final Map<String, Tool> toolMap = new HashMap<>();

    // Spring会自动注入所有实现了Tool接口的Bean
    public ToolManager(List<Tool> tools) {
        this.tools = tools;
    }

    @PostConstruct
    public void init() {
        for (Tool tool : tools) {
            toolMap.put(tool.getName(), tool);
        }
    }

    public Tool getTool(String name) {
        return toolMap.get(name);
    }

    public List<Tool> getAllTools() {
        return tools;
    }

    // 这个方法用于生成ReAct提示词中的 {{tools}} 部分
    public String generateToolsDescription() {
        StringBuilder sb = new StringBuilder();
        for (Tool tool : tools) {
            sb.append("- 工具名称:").append(tool.getName()).append("\n");
            sb.append("  描述:").append(tool.getDescription()).append("\n");
            sb.append("  参数:").append(tool.getParameters()).append("\n\n");
        }
        return sb.toString();
    }

    // 这个方法用于生成ReAct提示词中的 {{tool_names}} 部分
    public String generateToolNames() {
        return String.join(", ", toolMap.keySet());
    }
}

有了工具定义和管理器,我们就有了AI可以调用的“武器库”。下一步,就是构建那个最关键的“大脑”——ReAct提示词引擎,并实现循环调用逻辑。

5. 构建ReAct引擎:循环调用与结果解析

这是整个手动实现中最核心、也最需要耐心调试的部分。我们的目标很简单:写一个循环,每次循环都拿着构建好的对话历史(包含用户问题、AI的思考、行动、以及我们返回的观察结果)去调用DeepSeek R1 API,然后解析它的输出,判断是该执行工具,还是可以返回最终答案了。

首先,我们创建一个 ReActService 类。它会依赖Spring AI提供的 ChatClient 来与DeepSeek对话,以及我们刚才写的 ToolManager

import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.messages.*;
import org.springframework.ai.tool.Tool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;

@Service
public class ReActService {
    @Autowired
    private ChatClient chatClient;

    @Autowired
    private ToolManager toolManager;

    // 系统指令,可以根据你的场景修改
    private static final String SYSTEM_INSTRUCTION = "你是一个有帮助的AI助手,请严格遵循给定的格式进行思考和行动。";

    public String executeWithTools(String userQuestion) {
        // 1. 构建初始提示词
        String toolsDesc = toolManager.generateToolsDescription();
        String toolNames = toolManager.generateToolNames();

        String reactPromptTemplate = """
                尽可能帮助用户,并提供准确的回答。
                %s
                你可以使用以下工具:
                %s
                通过使用 JSON 对象来指定工具,提供 action(工具名称)和 action_input(工具输入)键。
                有效的 “action” 值有:“Final Answer” 或 %s。
                每个 $JSON_BLOB 只能指定一个动作,格式如下:
                ###
                {
                  "action": $TOOL_NAME,
                  "action_input": $ACTION_INPUT
                }
                ###
                遵循以下格式:
                问题(Question):要回答的输入问题
                思考(Thought):考虑之前和接下来的步骤
                行动(Action):
                ### $JSON_BLOB ###
                观察(Observation):行动结果
                … (重复“思考/行动/观察”多次,直至完成任务)
                思考(Thought):我知道如何回答了
                行动(Action):
                ###
                {
                  "action": "Final Answer",
                  "action_input": "Final response to human"
                }
                ###
                开始!请务必始终以有效的 JSON 对象形式提供单个Action。
                如果需要工具就使用工具。
                如果适合,请直接回应用户。
                格式为:###$JSON_BLOB###然后 Observation:.
                """.formatted(SYSTEM_INSTRUCTION, toolsDesc, toolNames);

        // 2. 初始化消息历史
        List<Message> messageHistory = new ArrayList<>();
        messageHistory.add(new SystemMessage(reactPromptTemplate));
        messageHistory.add(new UserMessage("问题(Question):" + userQuestion));

        // 3. 开始ReAct循环
        int maxIterations = 10; // 防止无限循环
        for (int i = 0; i < maxIterations; i++) {
            // 构建本次对话的Prompt
            Prompt prompt = new Prompt(messageHistory);
            // 调用DeepSeek R1
            ChatResponse response = chatClient.call(prompt);
            String assistantReply = response.getResult().getOutput().getContent();

            // 4. 解析AI的回复
            ParsedResult parsedResult = parseAssistantReply(assistantReply);
            System.out.println("第" + (i+1) + "轮回复解析结果:" + parsedResult);

            if ("Final Answer".equals(parsedResult.action)) {
                // 循环结束,返回最终答案
                return parsedResult.actionInput;
            }

            // 5. 执行工具调用
            Tool tool = toolManager.getTool(parsedResult.action);
            if (tool == null) {
                // 如果AI请求了一个不存在的工具,把错误信息作为Observation
                String errorObservation = "Observation: 工具 '" + parsedResult.action + "' 不存在。";
                messageHistory.add(new AssistantMessage(assistantReply));
                messageHistory.add(new UserMessage(errorObservation));
                continue;
            }

            String toolResult;
            try {
                toolResult = tool.call(parsedResult.actionInput);
            } catch (Exception e) {
                toolResult = "Observation: 调用工具 '" + parsedResult.action + "' 时出错: " + e.getMessage();
            }

            // 6. 将本次AI回复和工具执行结果加入历史,进行下一轮
            messageHistory.add(new AssistantMessage(assistantReply));
            messageHistory.add(new UserMessage("观察(Observation):" + toolResult));
        }

        return "经过多次尝试,未能得出最终答案。可能问题过于复杂或格式解析失败。";
    }

    // 解析AI回复的核心方法
    private ParsedResult parseAssistantReply(String reply) {
        ParsedResult result = new ParsedResult();
        // 首先,尝试提取Thought部分(不一定总是有,但有助于调试)
        int thoughtStart = reply.indexOf("思考(Thought):");
        if (thoughtStart != -1) {
            int thoughtEnd = reply.indexOf("\n", thoughtStart);
            if (thoughtEnd != -1) {
                result.thought = reply.substring(thoughtStart + "思考(Thought):".length(), thoughtEnd).trim();
            }
        }

        // 关键:提取被###包裹的JSON块
        String jsonBlockMarker = "###";
        int blockStart = reply.indexOf(jsonBlockMarker);
        if (blockStart == -1) {
            // 如果没有找到标记,可能AI直接给出了最终答案(非标准情况)
            result.action = "Final Answer";
            result.actionInput = reply;
            return result;
        }
        int blockEnd = reply.indexOf(jsonBlockMarker, blockStart + jsonBlockMarker.length());
        if (blockEnd == -1) {
            // 格式错误,标记不完整
            result.action = "Final Answer";
            result.actionInput = "错误:未找到匹配的JSON块结束标记。";
            return result;
        }

        String jsonStr = reply.substring(blockStart + jsonBlockMarker.length(), blockEnd).trim();
        try {
            ObjectMapper mapper = new ObjectMapper();
            JsonNode rootNode = mapper.readTree(jsonStr);
            result.action = rootNode.get("action").asText();
            JsonNode inputNode = rootNode.get("action_input");
            result.actionInput = (inputNode != null && !inputNode.isNull()) ? inputNode.asText() : "";
        } catch (Exception e) {
            // JSON解析失败
            result.action = "Final Answer";
            result.actionInput = "错误:无法解析Action JSON - " + e.getMessage() + "。原始内容:" + jsonStr;
        }
        return result;
    }

    // 内部类,用于存储解析结果
    private static class ParsedResult {
        String thought;
        String action;
        String actionInput;
        @Override
        public String toString() {
            return String.format("Thought: %s, Action: %s, Input: %s", thought, action, actionInput);
        }
    }
}

这段代码就是整个手动ReAct引擎的心脏。我来解释几个容易出错的点:

  1. 消息历史的构建:每一轮循环,我们都要把完整的对话历史(包括最初的系统提示、用户问题、AI的所有回复、我们所有的观察结果)传给模型。这样它才知道整个思考过程进行到哪一步了。Spring AI的 Message 对象帮我们很好地管理了角色(System, User, Assistant)。
  2. 解析AI回复:这是最脆弱的一环。我们依赖 ### 作为JSON块的标记。如果AI的输出里偶然出现了这三个字符,就会导致解析混乱。所以我在提示词里反复强调这个格式。解析时,先找标记,再提取中间的字符串,最后用Jackson解析JSON。一定要做好异常处理,因为AI的输出是不可控的。
  3. 循环终止条件:两个条件会终止循环:一是AI的action是 "Final Answer",二是达到了最大循环次数(我设为10,防止复杂任务陷入死循环)。在实际使用中,你可能还需要根据token数量或者超时时间来设置终止条件。
  4. 错误处理:如果AI调用了一个不存在的工具,或者工具执行抛异常,我们要把这个错误信息包装成 Observation 塞回对话历史。这样AI在下一轮思考时,就能知道上一步出错了,并调整策略。

写到这里,核心逻辑就完成了。你可以写一个简单的Controller来暴露一个HTTP接口,测试一下这个服务。

6. 实战测试与常见问题调试

理论完美,跑起来才是硬道理。我们写个测试Controller,然后看看实际效果。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/react")
public class ReActController {

    @Autowired
    private ReActService reActService;

    @PostMapping("/chat")
    public String chat(@RequestBody ChatRequest request) {
        // 简单起见,直接返回结果。生产环境应考虑流式响应、异步处理等。
        return reActService.executeWithTools(request.getQuestion());
    }

    // 简单的请求体
    public static class ChatRequest {
        private String question;
        // getter and setter ...
    }
}

启动应用,用Postman或者curl发个请求试试:

POST /api/react/chat
Content-Type: application/json

{
  "question": "北京今天的天气怎么样?然后再计算一下(25加上今天气温的数值)乘以2等于多少?"
}

理想情况下,你应该能在控制台看到类似这样的日志输出:

第1轮回复解析结果:Thought: 用户问了两个问题,先查询天气,再计算。 Action: get_weather, Input: {"city":"北京"}
观察(Observation):城市【北京】的天气情况:晴,温度25℃,湿度60%,东南风2级。
第2轮回复解析结果:Thought: 已获取北京气温为25℃。现在需要计算 (25 + 25) * 2。 Action: calculator, Input: {"expression":"(25+25)*2"}
观察(Observation):表达式 `(25+25)*2` 的计算结果是:100.0
第3轮回复解析结果:Thought: 我已经获得了所有必要信息。 Action: Final Answer, Input: 北京今天天气晴朗,气温25℃。根据您的要求,(25 + 25) * 2 的计算结果是100。

最终返回给用户的,就是最后那个 Final Answer 里的内容。

当然,现实很骨感,你可能会遇到各种问题。下面是我踩过的一些坑和解决办法:

问题1:AI不按格式输出,解析失败。

  • 原因:提示词不够严格,或者temperature参数太高。
  • 解决:反复检查并强化提示词中的格式要求,特别是分隔符 ### 和关键词(Thought, Action, Observation)。将API调用的 temperature 参数设为0或接近0的值。你甚至可以在提示词开头加上“你必须严格遵守以下格式,任何偏离都将导致错误。”

问题2:AI在应该调用工具时,直接输出了最终答案。

  • 原因:可能是工具描述不够清晰,AI觉得靠自己已有的知识就能回答。
  • 解决:在系统指令({{instruction}})里明确强调“对于涉及实时信息、计算或外部数据的问题,你必须使用提供的工具。”同时,检查工具的描述是否准确说明了其能力边界。

问题3:循环卡住,一直在调用同一个工具。

  • 原因:工具返回的观察结果可能没有提供足够的新信息,或者AI陷入了逻辑循环。
  • 解决:在工具返回的观察结果中,提供更结构化、更清晰的信息。如果循环超过一定次数(比如5次)还没有得到Final Answer,可以主动终止并返回一个错误提示,或者将历史记录给用户看,进行人工干预。

问题4:JSON解析出错,因为AI输出的JSON里有换行或多余字符。

  • 解决:在解析前,对提取出的JSON字符串进行清洗,比如去除首尾空白、删除JSON标记外的所有内容。使用健壮的JSON库(如Jackson)并做好 try-catch

调试这个过程有点像训练一个遵守纪律的新手。你需要非常明确、甚至有点啰嗦地告诉它每一步该做什么、该怎么说。一旦它“上了道”,跑起来就会非常顺畅。

7. 进阶优化与生产环境考量

如果上面的基础版本你已经跑通了,恭喜你!但要想用到实际项目里,还有一些地方可以优化,让整个系统更健壮、更高效。

1. 提示词工程优化: 我们现在的提示词是硬编码在Java代码里的,维护起来不方便。更好的做法是把它放到外部配置文件(如YAML)或者数据库中。可以针对不同的任务类型(数据分析、客服、内容生成)准备不同的提示词模板,动态加载。

2. 支持流式输出: 用户不想等整个循环结束才看到答案。Spring AI的 ChatClient 支持流式响应(stream() 方法)。我们可以改造 ReActService,每次AI输出 ThoughtAction 时,就通过Server-Sent Events (SSE) 或WebSocket推送给前端,让用户看到AI的“思考过程”,体验会好很多。

3. 工具调用的增强:

  • 参数验证与转换:现在的工具 call 方法直接接收字符串。最好能定义一个强类型的请求对象,在调用前进行验证和类型转换。
  • 工具编排:有些复杂任务需要按特定顺序调用多个工具。可以在 ToolManager 里加入简单的编排逻辑,或者利用更高级的框架(如LangChain4J)的Chain功能。
  • 工具结果后处理:工具返回的原始数据(比如一大段JSON)直接塞给AI可能效果不好。可以写一个后处理器,将原始结果总结、提炼成更精炼的文本,再作为 Observation

4. 对话历史管理与上下文长度: DeepSeek R1有上下文长度限制。我们的ReAct循环会在历史中附加很多轮消息,很容易超限。需要实现一个“摘要”功能,当历史消息太长时,将早期的“思考-行动-观察”循环压缩成一段简短的摘要,只保留最近几轮和关键的观察结果。

5. 加入Fallback机制: 如果我们的手动ReAct解析连续失败,或者AI始终不按格式输出,应该有一个降级方案。比如,直接回退到普通的ChatCompletion模式,告诉用户“暂时无法使用工具,但我可以尝试直接回答您的问题”。

6. 监控与日志: 在生产环境,详细的日志至关重要。要记录每一轮循环的输入输出、工具调用耗时、解析成功与否。这不仅能帮你快速定位问题,还能用来分析AI在哪些情况下容易“不听话”,从而进一步优化提示词。

手动实现DeepSeek R1的工具调用,一开始会觉得有点绕,但一旦你把整个流程串起来,会发现思路其实非常清晰。它让你对AI如何与外部世界交互有了更底层的理解。虽然等官方支持后,我们大概率会切换到更优雅的原生工具调用方式,但这段手动实现的经历,对于理解ReAct框架和LLM的工作机制,绝对是宝贵的实战经验。至少下次遇到类似需求,你心里有底了。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐