DeepSeek Function Calling 接入实时行情:从工具定义到多轮查询的完整示例

目录

  • [30 秒结论](#30 秒结论 "#30-%E7%A7%92%E7%BB%93%E8%AE%BA")
  • [一、Function Calling 原理:模型不执行代码,它只"下命令"](#一、Function Calling 原理:模型不执行代码,它只“下命令” "#%E4%B8%80function-calling-%E5%8E%9F%E7%90%86%E6%A8%A1%E5%9E%8B%E4%B8%8D%E6%89%A7%E8%A1%8C%E4%BB%A3%E7%A0%81%E5%AE%83%E5%8F%AA%E4%B8%8B%E5%91%BD%E4%BB%A4")
  • [二、tools 参数完整设计](#二、tools 参数完整设计 "#%E4%BA%8Ctools-%E5%8F%82%E6%95%B0%E5%AE%8C%E6%95%B4%E8%AE%BE%E8%AE%A1")
  • [三、完整可运行 Python 代码](#三、完整可运行 Python 代码 "#%E4%B8%89%E5%AE%8C%E6%95%B4%E5%8F%AF%E8%BF%90%E8%A1%8C-python-%E4%BB%A3%E7%A0%81")
  • 四、完整对话轨迹示例
  • [五、Strict Function Calling(进阶)](#五、Strict Function Calling(进阶) "#%E4%BA%94strict-function-calling%E8%BF%9B%E9%98%B6")
  • 六、多轮对话进阶:让模型自己规划任务
  • 七、错误处理与工程化建议
  • [八、为什么用 REST + Function Calling,而不是 MCP](#八、为什么用 REST + Function Calling,而不是 MCP "#%E5%85%AB%E4%B8%BA%E4%BB%80%E4%B9%88%E7%94%A8-rest--function-calling%E8%80%8C%E4%B8%8D%E6%98%AF-mcp")
  • 九、工具选型速查表

30 秒结论

DeepSeek 不会主动查股票------但你可以给它一套工具定义(tools 参数),让它判断何时调用、传什么参数。

本文用 DeepSeek Function Calling API + TickDB REST API,从零搭建一个可真实调用行情数据的查询系统。你将得到:完整 Python 代码、3 个核心工具的 JSON Schema 设计、完整对话轨迹、多轮任务规划示例,以及 Strict Function Calling 模式的使用方法(含常见错误)。


一、Function Calling 原理:模型不执行代码,它只"下命令"

DeepSeek 的训练数据有截止日期。你问它"茅台现在多少钱",它只能给你一个过去某个时间点的价格------甚至直接编一个。

Function Calling 的机制解决了这个问题。它不靠模型自己"知道"数据,而是靠一套四步循环:

复制代码
① 用户提出问题
② 模型返回 tool_calls(一个结构化的函数调用请求,不是文本回复)
③ 你的 Python 代码执行这个函数------真的去调行情 API,拿到真实数据
④ 把数据返回给模型,模型基于真实数据生成最终文本回复

关键认知 :模型不执行任何代码。它只说"我想调用 get_ticker,参数是 symbols="700.HK""------真正发 HTTP 请求的是你写的 Python 代码。数据从哪里来、质量如何、是否及时,完全由你控制。


二、tools 参数完整设计

tools 参数是 Function Calling 的灵魂。设计太粗糙,模型不知道传什么参数;设计太复杂,模型容易调用失败。

以下基于 TickDB REST API,设计 3 个核心工具。

工具与端点映射

工具名 TickDB REST 端点 用途
get_ticker GET /v1/market/ticker 实时行情快照
get_kline GET /v1/market/kline 历史 K 线数据
get_stock_info GET /v1/market/stock-info 股票基本面(EPS/每股净资产/股息率/股本)

工具 1:get_ticker(实时行情快照)

python 复制代码
{
    "type": "function",
    "function": {
        "name": "get_ticker",
        "description": "获取一个或多个交易品种的实时行情快照,包括最新价、涨跌幅、24小时最高最低价、成交量。"
                       "支持 A 股(.SH/.SZ)、港股(.HK)、美股(.US)、加密货币(如 BTCUSDT)、外汇(如 EURUSD)、"
                       "贵金属(XAUUSD)、指数(SPX)。当用户想了解某品种的当前价格或涨跌情况时,使用此工具。"
                       "当用户询问历史走势、K线形态、技术指标时,不要使用此工具,应使用 get_kline。"
                       "注意:此工具支持多个品种同时查询(symbols 参数用逗号分隔),最多 50 个,超过需分批调用。",
        "parameters": {
            "type": "object",
            "properties": {
                "symbols": {
                    "type": "string",
                    "description": "交易品种代码,多个用英文逗号分隔,最多 50 个。格式示例:'AAPL.US' 或 'BTCUSDT,700.HK,600519.SH'。"
                                   "A 股格式为 6 位数字+.SH/.SZ(如 600519.SH),港股格式为数字+.HK 无前导零(如 700.HK),"
                                   "美股格式为字母+.US(如 AAPL.US),加密货币直接写币对(如 BTCUSDT),外汇写 6 位货币对(如 EURUSD),"
                                   "贵金属写 XAUUSD/XAGUSD,指数写 3-4 位代码无后缀(如 SPX/NDX/DJI)。"
                }
            },
            "required": ["symbols"],
            "additionalProperties": False
        }
    }
}

工具 2:get_kline(历史 K 线数据)

python 复制代码
{
    "type": "function",
    "function": {
        "name": "get_kline",
        "description": "获取某个交易品种的历史 K 线数据(OHLCV),返回开盘价、最高价、最低价、收盘价、成交量。"
                       "适合用于技术分析、趋势观察、计算技术指标。当用户提及 K 线、蜡烛图、趋势分析、历史走势或技术指标时,使用此工具。"
                       "当用户仅询问当前价格、涨跌幅时,不要使用此工具,应使用 get_ticker。"
                       "注意:此工具只接受单个品种代码(symbol 参数为单数)。当用户需要同时查询多个品种的实时行情时,应使用 get_ticker 而非此工具。"
                       "未指定周期默认 1d(日线),未指定数量默认 20 根。",
        "parameters": {
            "type": "object",
            "properties": {
                "symbol": {
                    "type": "string",
                    "description": "单个交易品种代码。格式示例:'700.HK' 或 'AAPL.US' 或 'BTCUSDT'。注意只能传入一个品种代码,不支持逗号分隔。"
                },
                "interval": {
                    "type": "string",
                    "enum": ["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "1d", "1w", "1M"],
                    "description": "K 线周期。1m=1分钟,3m=3分钟,5m=5分钟,15m=15分钟,30m=30分钟,1h=1小时,2h=2小时,4h=4小时,1d=日线,1w=周线,1M=月线。"
                },
                "limit": {
                    "type": "integer",
                    "description": "返回 K 线数量。默认 20。",
                    "minimum": 1,
                    "maximum": 200
                }
            },
            "required": ["symbol", "interval"],
            "additionalProperties": False
        }
    }
}

工具 3:get_stock_info(股票基本面)

python 复制代码
{
    "type": "function",
    "function": {
        "name": "get_stock_info",
        "description": "获取股票的基本面数据,包括公司名称、每手股数、总股本、流通股本、每股盈利(EPS TTM)、每股净资产、股息率。"
                       "当用户询问某只股票的基本信息、每股数据、分红情况、每手股数时,使用此工具。"
                       "注意:此工具不返回市盈率(PE)、市净率(PB)、总市值等估值指标------这些估值数据应使用 get_market_metrics 工具查询。"
                       "此工具只适用于股票(A 股/港股/美股),不适用于加密货币、外汇、贵金属或指数。"
                       "如果用户询问的是实时价格或 K 线走势,应使用 get_ticker 或 get_kline,不要使用此工具。",
        "parameters": {
            "type": "object",
            "properties": {
                "symbols": {
                    "type": "string",
                    "description": "股票代码,多个用英文逗号分隔,最多 50 个。格式示例:'700.HK,AAPL.US,600519.SH'。只支持股票类品种。"
                }
            },
            "required": ["symbols"],
            "additionalProperties": False
        }
    }
}

设计要点

  • description 决定模型会不会选对工具 :不只写"获取行情",而是写清楚适用场景和不触发条件(如"当用户询问历史走势时,不要使用此工具,应使用 get_kline")。description 中还包含默认值提示,帮助模型在模糊场景下做出正确选择。
  • enum 约束可选值 :K 线周期用 enum 限制合法取值(含 3m),避免模型传不存在的周期。
  • required 只标真正必需的limit 有默认值 20,不加 required,加了反而可能因模型没传而调用失败。
  • 参数 description 包含格式示例和反例 :如 get_klinesymbol 描述中明确"只能传入一个品种代码,不支持逗号分隔"。
  • limit 增加数值约束minimum: 1maximum: 200,避免模型传无效值。

常见错误示范

错误用法 错误原因 正确写法
get_kline(symbols="700.HK,AAPL.US") 传了复数 symbols 参数 get_kline(symbol="700.HK"),多品种需多次调用
get_stock_info(symbols="BTCUSDT") 传了加密货币代码 get_ticker(symbols="BTCUSDT") 查加密行情
get_ticker(symbols="0700.HK") 港股代码加了前导零 get_ticker(symbols="700.HK")

三、完整可运行 Python 代码

环境准备

bash 复制代码
pip install openai requests

设置环境变量:

bash 复制代码
export DEEPSEEK_API_KEY="your-deepseek-api-key"
export DEEPSEEK_MODEL="deepseek-v4-pro"    # 以 DeepSeek 官方文档最新模型名为准
export TICKDB_API_KEY="your-tickdb-api-key"

完整代码

python 复制代码
import json
import os
import time
from decimal import Decimal, InvalidOperation
import requests
from openai import OpenAI

# ========== 配置区 ==========
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
DEEPSEEK_MODEL = os.getenv("DEEPSEEK_MODEL", "deepseek-v4-pro")
TICKDB_API_KEY = os.getenv("TICKDB_API_KEY")

if not DEEPSEEK_API_KEY or not TICKDB_API_KEY:
    raise ValueError("请设置环境变量 DEEPSEEK_API_KEY 和 TICKDB_API_KEY")

# DeepSeek 客户端(兼容 OpenAI SDK 格式)
client = OpenAI(
    api_key=DEEPSEEK_API_KEY,
    base_url="https://api.deepseek.com",
)

# ========== tools 定义 ==========
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_ticker",
            "description": (
                "获取一个或多个交易品种的实时行情快照,包括最新价、涨跌幅、24小时最高最低价、成交量。"
                "支持 A 股(.SH/.SZ)、港股(.HK)、美股(.US)、加密货币(如 BTCUSDT)、外汇(如 EURUSD)、"
                "贵金属(XAUUSD)、指数(SPX)。当用户想了解某品种的当前价格或涨跌情况时,使用此工具。"
                "当用户询问历史走势、K线形态、技术指标时,不要使用此工具,应使用 get_kline。"
                "注意:此工具支持多个品种同时查询(symbols 参数用逗号分隔),最多 50 个,超过需分批调用。"
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "symbols": {
                        "type": "string",
                        "description": (
                            "交易品种代码,多个用英文逗号分隔,最多 50 个。格式示例:'AAPL.US' 或 'BTCUSDT,700.HK,600519.SH'。"
                            "A 股格式为 6 位数字+.SH/.SZ(如 600519.SH),港股格式为数字+.HK 无前导零(如 700.HK),"
                            "美股格式为字母+.US(如 AAPL.US),加密货币直接写币对(如 BTCUSDT),外汇写 6 位货币对(如 EURUSD),"
                            "贵金属写 XAUUSD/XAGUSD,指数写 3-4 位代码无后缀(如 SPX/NDX/DJI)。"
                        )
                    }
                },
                "required": ["symbols"],
                "additionalProperties": False
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_kline",
            "description": (
                "获取某个交易品种的历史 K 线数据(OHLCV),返回开盘价、最高价、最低价、收盘价、成交量。"
                "适合用于技术分析、趋势观察、计算技术指标。当用户提及 K 线、蜡烛图、趋势分析、历史走势或技术指标时,使用此工具。"
                "当用户仅询问当前价格、涨跌幅时,不要使用此工具,应使用 get_ticker。"
                "注意:此工具只接受单个品种代码(symbol 参数为单数)。当用户需要同时查询多个品种的实时行情时,应使用 get_ticker 而非此工具。"
                "未指定周期默认 1d(日线),未指定数量默认 20 根。"
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "symbol": {
                        "type": "string",
                        "description": "单个交易品种代码。格式示例:'700.HK' 或 'AAPL.US' 或 'BTCUSDT'。注意只能传入一个品种代码,不支持逗号分隔。"
                    },
                    "interval": {
                        "type": "string",
                        "enum": ["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "1d", "1w", "1M"],
                        "description": "K 线周期。1m=1分钟,3m=3分钟,5m=5分钟,15m=15分钟,30m=30分钟,1h=1小时,2h=2小时,4h=4小时,1d=日线,1w=周线,1M=月线。"
                    },
                    "limit": {
                        "type": "integer",
                        "description": "返回 K 线数量。默认 20。",
                        "minimum": 1,
                        "maximum": 200
                    }
                },
                "required": ["symbol", "interval"],
                "additionalProperties": False
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_stock_info",
            "description": (
                "获取股票的基本面数据,包括公司名称、每手股数、总股本、流通股本、每股盈利(EPS TTM)、每股净资产、股息率。"
                "当用户询问某只股票的基本信息、每股数据、分红情况、每手股数时,使用此工具。"
                "注意:此工具不返回市盈率(PE)、市净率(PB)、总市值等估值指标------这些估值数据应使用 get_market_metrics 工具查询。"
                "此工具只适用于股票(A 股/港股/美股),不适用于加密货币、外汇、贵金属或指数。"
                "如果用户询问的是实时价格或 K 线走势,应使用 get_ticker 或 get_kline,不要使用此工具。"
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "symbols": {
                        "type": "string",
                        "description": "股票代码,多个用英文逗号分隔,最多 50 个。格式示例:'700.HK,AAPL.US,600519.SH'。只支持股票类品种。"
                    }
                },
                "required": ["symbols"],
                "additionalProperties": False
            }
        }
    }
]

# ========== 工具执行函数 ==========
def safe_decimal(value, default=None):
    """安全转换为 Decimal,失败时返回默认值"""
    try:
        return Decimal(str(value))
    except (InvalidOperation, ValueError, TypeError):
        return default


def execute_get_ticker(symbols: str) -> str:
    """调用 TickDB REST API 获取实时行情"""
    url = "https://api.tickdb.ai/v1/market/ticker"
    headers = {"X-API-Key": TICKDB_API_KEY}
    params = {"symbols": symbols}

    for attempt in range(3):
        try:
            resp = requests.get(url, headers=headers, params=params, timeout=10)

            # HTTP 429 限流
            if resp.status_code == 429:
                retry_after = resp.headers.get("Retry-After")
                wait = int(retry_after) if retry_after else (2 ** attempt)
                if attempt < 2:
                    time.sleep(wait)
                    continue
                return json.dumps({"error": "rate_limited", "message": f"HTTP 429 限流,请 {wait}s 后重试"})

            data = resp.json()

            if data["code"] == 0:
                result = []
                for item in data["data"]:
                    result.append({
                        "symbol": item["symbol"],
                        "last_price": item["last_price"],
                        "price_change_percent_24h": item.get("price_change_percent_24h", "N/A"),
                        "volume_24h": item.get("volume_24h", "N/A"),
                        "high_24h": item.get("high_24h", "N/A"),
                        "low_24h": item.get("low_24h", "N/A"),
                    })
                return json.dumps(result, ensure_ascii=False)

            elif data["code"] == 3001:
                if attempt < 2:
                    retry_after = resp.headers.get("Retry-After")
                    wait = int(retry_after) if retry_after else (2 ** attempt)
                    time.sleep(wait)
                    continue
                return json.dumps({"error": "rate_limited", "message": "请求频率超限,请稍后重试"})

            elif data["code"] == 2002:
                return json.dumps({"error": "symbol_not_found", "message": "品种代码不存在,请检查格式"})

            elif data["code"] == 1001:
                return json.dumps({"error": "auth_error", "code": 1001, "message": "API Key 无效或已过期"})
            elif data["code"] == 1002:
                return json.dumps({"error": "auth_error", "code": 1002, "message": "未提供 API Key"})
            elif data["code"] == 1004:
                return json.dumps({"error": "auth_error", "code": 1004, "message": "API Key 权限不足"})

            else:
                return json.dumps({"error": "api_error", "code": data["code"], "message": data.get("message", "")})

        except requests.exceptions.Timeout:
            if attempt < 2:
                continue
            return json.dumps({"error": "timeout", "message": "数据源响应超时"})
        except Exception as e:
            return json.dumps({"error": "exception", "message": str(e)})

    return json.dumps({"error": "unknown", "message": "未知错误"})


def execute_get_kline(symbol: str, interval: str, limit: int = 20) -> str:
    """调用 TickDB REST API 获取历史 K 线"""
    url = "https://api.tickdb.ai/v1/market/kline"
    headers = {"X-API-Key": TICKDB_API_KEY}
    params = {"symbol": symbol, "interval": interval, "limit": limit}

    for attempt in range(3):
        try:
            resp = requests.get(url, headers=headers, params=params, timeout=10)

            if resp.status_code == 429:
                retry_after = resp.headers.get("Retry-After")
                wait = int(retry_after) if retry_after else (2 ** attempt)
                if attempt < 2:
                    time.sleep(wait)
                    continue
                return json.dumps({"error": "rate_limited"})

            data = resp.json()

            if data["code"] == 0:
                klines = data["data"]["klines"]
                result = []
                for k in klines[-limit:]:
                    result.append({
                        "time": k["time"],
                        "open": k["open"],
                        "high": k["high"],
                        "low": k["low"],
                        "close": k["close"],
                        "volume": k["volume"]
                    })
                return json.dumps(result, ensure_ascii=False)

            elif data["code"] == 3001:
                if attempt < 2:
                    retry_after = resp.headers.get("Retry-After")
                    wait = int(retry_after) if retry_after else (2 ** attempt)
                    time.sleep(wait)
                    continue
                return json.dumps({"error": "rate_limited"})

            elif data["code"] == 1001:
                return json.dumps({"error": "auth_error", "code": 1001, "message": "API Key 无效或已过期"})
            elif data["code"] == 1002:
                return json.dumps({"error": "auth_error", "code": 1002, "message": "未提供 API Key"})
            elif data["code"] == 1004:
                return json.dumps({"error": "auth_error", "code": 1004, "message": "API Key 权限不足"})

            else:
                return json.dumps({"error": "api_error", "code": data["code"]})

        except requests.exceptions.Timeout:
            if attempt < 2:
                continue
            return json.dumps({"error": "timeout"})
        except Exception as e:
            return json.dumps({"error": "exception", "message": str(e)})

    return json.dumps({"error": "unknown"})


def execute_get_stock_info(symbols: str) -> str:
    """调用 TickDB REST API 获取股票基本面"""
    url = "https://api.tickdb.ai/v1/market/stock-info"
    headers = {"X-API-Key": TICKDB_API_KEY}
    params = {"symbols": symbols}

    for attempt in range(3):
        try:
            resp = requests.get(url, headers=headers, params=params, timeout=10)

            if resp.status_code == 429:
                retry_after = resp.headers.get("Retry-After")
                wait = int(retry_after) if retry_after else (2 ** attempt)
                if attempt < 2:
                    time.sleep(wait)
                    continue
                return json.dumps({"error": "rate_limited"})

            data = resp.json()

            if data["code"] == 0:
                result = []
                for item in data["data"]:
                    result.append({
                        "symbol": item["symbol"],
                        "name_cn": item.get("name_cn", "N/A"),
                        "eps_ttm": item.get("eps_ttm", "N/A"),
                        "bps": item.get("bps", "N/A"),
                        "dividend_yield": item.get("dividend_yield", "N/A"),
                        "lot_size": item.get("lot_size", "N/A"),
                        "total_shares": item.get("total_shares", "N/A"),
                        "circulating_shares": item.get("circulating_shares", "N/A"),
                    })
                return json.dumps(result, ensure_ascii=False)

            elif data["code"] == 3001:
                if attempt < 2:
                    retry_after = resp.headers.get("Retry-After")
                    wait = int(retry_after) if retry_after else (2 ** attempt)
                    time.sleep(wait)
                    continue
                return json.dumps({"error": "rate_limited"})

            elif data["code"] == 1001:
                return json.dumps({"error": "auth_error", "code": 1001, "message": "API Key 无效或已过期"})
            elif data["code"] == 1002:
                return json.dumps({"error": "auth_error", "code": 1002, "message": "未提供 API Key"})
            elif data["code"] == 1004:
                return json.dumps({"error": "auth_error", "code": 1004, "message": "API Key 权限不足"})

            else:
                return json.dumps({"error": "api_error", "code": data["code"]})

        except requests.exceptions.Timeout:
            if attempt < 2:
                continue
            return json.dumps({"error": "timeout"})
        except Exception as e:
            return json.dumps({"error": "exception", "message": str(e)})

    return json.dumps({"error": "unknown"})


# ========== 工具调度器 ==========
TOOL_EXECUTORS = {
    "get_ticker": execute_get_ticker,
    "get_kline": execute_get_kline,
    "get_stock_info": execute_get_stock_info,
}


def run_conversation(user_query: str, messages: list):
    """执行一轮对话(可能触发多轮工具调用)"""
    messages.append({"role": "user", "content": user_query})

    for _ in range(5):  # 最多 5 轮,防止死循环
        response = client.chat.completions.create(
            model=DEEPSEEK_MODEL,
            messages=messages,
            tools=TOOLS
        )

        assistant_message = response.choices[0].message

        if not assistant_message.tool_calls:
            messages.append({"role": "assistant", "content": assistant_message.content})
            return assistant_message.content

        messages.append(assistant_message)

        for tool_call in assistant_message.tool_calls:
            func_name = tool_call.function.name

            # 安全解析 JSON 参数(DeepSeek 官方提醒 arguments 不一定总是合法 JSON)
            try:
                func_args = json.loads(tool_call.function.arguments)
            except json.JSONDecodeError as e:
                error_result = json.dumps({
                    "error": "invalid_arguments",
                    "message": f"工具参数 JSON 解析失败: {str(e)}",
                    "raw_arguments": tool_call.function.arguments
                })
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": error_result
                })
                continue

            print(f"[Tool Call] {func_name}({func_args})")

            executor = TOOL_EXECUTORS.get(func_name)
            if executor:
                result = executor(**func_args)
            else:
                result = json.dumps({"error": "unknown_tool", "message": f"未知工具: {func_name}"})

            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result
            })

    return "达到最大工具调用轮数,请简化查询。"


# ========== main ==========
if __name__ == "__main__":
    print("=" * 60)
    print("示例 1:查实时行情")
    result = run_conversation(
        "帮我查一下腾讯(700.HK)和苹果(AAPL.US)的最新股价和涨跌幅。",
        messages=[]
    )
    print(result)

    print("\n" + "=" * 60)
    print("示例 2:拉 K 线并计算均线")
    result = run_conversation(
        "帮我拉取比特币(BTCUSDT)最近 10 根日 K 线的收盘价,计算 5 日均线。"
        "注意价格字段是字符串格式,需要先转换为 Decimal 再计算。",
        messages=[]
    )
    print(result)

    print("\n" + "=" * 60)
    print("示例 3:跨市场基本面对比")
    result = run_conversation(
        "帮我对比腾讯(700.HK)和茅台(600519.SH)的基本面:每手股数、EPS、股息率。",
        messages=[]
    )
    print(result)

核心解读 :代码的核心是 run_conversation() 中的状态判断------if not assistant_message.tool_calls 是区分"模型要调工具"和"模型直接回答"的关键分支。tools 定义中的 description 决定了模型能否选对工具------description 写清楚"什么时候用这个工具、不支持什么场景、什么时候用别的工具",比参数的 JSON Schema 本身更重要。json.loads() 外的 try-except 是生产环境必须的防御------DeepSeek 官方明确指出 tool arguments 不一定总是合法 JSON。


四、完整对话轨迹示例

以下是"查腾讯和苹果股价"的实际消息流:

【用户输入】

scss 复制代码
"帮我查一下腾讯(700.HK)和苹果(AAPL.US)的最新股价和涨跌幅。"

【DeepSeek 返回的 tool_calls】

json 复制代码
{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "tool_calls": [{
        "function": {
          "name": "get_ticker",
          "arguments": "{\"symbols\":\"700.HK,AAPL.US\"}"
        }
      }]
    },
    "finish_reason": "tool_calls"
  }]
}

【工具返回结果】(TickDB REST API 返回后经代码精简)

json 复制代码
[
  {"symbol": "700.HK", "last_price": "455.20", "price_change_percent_24h": "0.55"},
  {"symbol": "AAPL.US", "last_price": "198.45", "price_change_percent_24h": "-0.82"}
]

注意price_change_percent_24h 返回的是字符串数值(如 "0.55"),不是已格式化的 +2.15%。是否需要加 % 应由展示层处理。

【DeepSeek 最终回复】

scss 复制代码
腾讯控股(700.HK)最新价:455.20 港元,涨跌幅 0.55%
苹果(AAPL.US)最新价:198.45 美元,涨跌幅 -0.82%

这个轨迹清晰展示了四步循环:userassistant(tool_calls)toolassistant(text)


五、Strict Function Calling(进阶)

DeepSeek 在 Beta API 中支持 Strict Function Calling,服务端强制校验 JSON 输出与 Schema 的匹配。

启用方式:

python 复制代码
client = OpenAI(
    api_key=DEEPSEEK_API_KEY,
    base_url="https://api.deepseek.com/beta",  # Beta 地址
)

Strict 模式的关键规则

  1. additionalProperties 必须设为 False
  2. 所有 properties 中定义的字段,必须出现在 required 数组中
  3. 不能使用 oneOfanyOf 等不支持的类型

常见 Strict 模式错误

如果你把普通模式的 schema 直接加 strict: true,会触发服务端 400 错误。最典型的例子是 get_klinelimit 字段:

python 复制代码
# 错误:limit 在 properties 但不在 required
{
    "properties": {
        "symbol": {...},
        "interval": {...},
        "limit": {...}       # ← 在 properties 中
    },
    "required": ["symbol", "interval"]  # ← 但不在 required 中
}
# DeepSeek Beta 返回 400 错误:
# "additionalProperties: False but 'limit' not in required"

两种改法

改法 做法 适用场景
A limit 加入 required:"required": ["symbol", "interval", "limit"] 希望模型每次明确传 limit
B 从 properties 中移除 limit,函数内硬编码默认值 不想让模型决定 limit

建议先用普通模式验证设计合理后,再迁移到 Strict 模式。


六、多轮对话进阶:让模型自己规划任务

Function Calling 的真正威力在于多轮任务规划。同一个对话窗口内,模型可以连续调用多个不同工具,并且能利用历史消息中的指代关系。

示例场景:

用户:帮我分析一下腾讯(700.HK)。先查实时行情,再拉最近 10 根日 K 线,最后查一下它的基本面。

DeepSeek 的规划过程:

轮次 模型行为 调用的工具
第 1 轮 识别到"实时行情" get_ticker(symbols="700.HK")
第 2 轮 识别到"日 K 线" get_kline(symbol="700.HK", interval="1d", limit=10)
第 3 轮 识别到"基本面" get_stock_info(symbols="700.HK")
第 4 轮 所有数据就绪 生成综合回复

多轮对话的 messages 累积示例

关键在于 messages 列表跨轮次传递,模型从历史消息中解析上下文和指代关系:

python 复制代码
# 初始化空的 messages 列表
messages = []

# 第一轮:明确指定品种
print("=== 第一轮 ===")
result1 = run_conversation("查腾讯(700.HK)实时行情", messages)
# messages 现在包含:
#   user: "查腾讯(700.HK)实时行情"
#   assistant(tool_call): get_ticker(symbols="700.HK")
#   tool: [{"symbol":"700.HK","last_price":"455.20",...}]
#   assistant(text): "腾讯控股最新价:455.20 港元..."
print(result1)

# 第二轮:使用指代词,模型从历史消息中解析"它"=700.HK
print("\n=== 第二轮 ===")
result2 = run_conversation("再拉一下它的日 K 线,最近 10 根", messages)
# 模型从 messages 中识别"它"=700.HK,调用 get_kline(symbol="700.HK", interval="1d", limit=10)
print(result2)

# 第三轮:继续在同一上下文内查询基本面
print("\n=== 第三轮 ===")
result3 = run_conversation("查一下它的基本面数据", messages)
# 模型调用 get_stock_info(symbols="700.HK")
print(result3)

关键机制 :每次 run_conversation() 调用后,messages 列表会累积 userassistanttool 三种角色的消息。下一轮调用时,模型能从完整的历史消息中解析出之前查询的品种、数据内容以及指代关系。

Thinking Mode + Tool Calls 提示 :如果同时启用 DeepSeek 的 Thinking Mode,tool call 后需要正确保留相关的 assistant message。详见 DeepSeek Tool Calls 文档


七、错误处理与工程化建议

常见错误处理(已在上文代码中实现)

错误场景 处理策略
HTTP 429 / 业务码 3001 限流 读取 Retry-After 头,指数退避重试,最多 3 次
品种代码不存在(2002) 返回错误信息,模型能理解并提示用户检查代码
鉴权失败(1001=Key无效/过期,1002=未提供Key,1004=权限不足) 分别返回具体错误含义
网络超时 timeout=10,最多重试 3 次
tool_call.function.arguments JSON 解析失败 try-except 捕获,返回原始参数和错误信息

工程化建议

  1. tools 定义与执行函数分离:tools 定义放 JSON 文件,执行函数放独立模块。新增工具时改配置不碰代码。

  2. tool_call_id 精确关联:每个 tool_call 有唯一 ID,返回结果时必须带上,模型靠这个 ID 关联请求和结果。

  3. 对话历史长度控制:多轮对话会导致 messages 超出上下文限制。保留最近 20 轮,早期消息做摘要压缩。

  4. 价格计算用 Decimal :金融场景下价格和数量的计算优先使用 Decimal,避免 float 精度问题。代码中的 safe_decimal() 工具函数可用于从 API 返回的字符串安全转换。


八、为什么用 REST + Function Calling,而不是 MCP

本文选择用 REST API 手写工具定义和 HTTP 调用,而不是直接使用 MCP Server,有一个明确的理由:Function Calling 让你看到工具的完整封装过程

对比维度 REST + Function Calling MCP Server
工具定义 你手写 JSON Schema,完全控制 description 服务端预定义,客户端自动发现
HTTP 调用 你写 requests.get(),控制超时/重试/错误映射 MCP 中间层自动完成
适用场景 需要精确控制工具行为、自定义错误处理 需要快速集成、不想写 HTTP 调用代码
学习价值 理解 Function Calling 的完整机制 理解 MCP 协议和客户端配置

结论 :如果你想理解 Function Calling 的每一个环节------从 Schema 设计到错误处理到多轮 messages 累积------REST 方案是最佳学习路径。如果你已经理解了原理,想在 AI 编程工具中直接使用行情工具,TickDB 也提供了 MCP Server(https://mcp.tickdb.ai/),在 Claude Code、Cursor、Codex 等工具中配置即可,13 个标准化工具自动发现,不需要手写 JSON Schema。


九、工具选型速查表

用户意图 工具 参数 TickDB 端点 返回路径
查当前价格/涨跌幅 get_ticker symbols="700.HK" GET /v1/market/ticker data[]
拉 K 线/技术分析 get_kline symbol="700.HK", interval="1d", limit=20 GET /v1/market/kline data.klines[]time/open/high/low/close/volume
查基本面/EPS/股息率 get_stock_info symbols="700.HK" GET /v1/market/stock-info data[]name_cn/eps_ttm/bps/dividend_yield
查 PE/PB/市值 get_market_metrics symbols="700.HK" GET /v1/market/market-metrics data[]pe_ttm_ratio/pb_ratio

字段使用提醒

  • ticker 端点 → 价格字段为 last_price
  • kline 端点 → 价格字段为 open/high/low/close
  • 两者不可跨端点混用------从 K 线数据里取 last_price 会报 KeyError,从 ticker 里取 close 同理。

你在用 DeepSeek 接实时数据时,遇到过模型选错工具或编造数据的情况吗?评论区聊。


📡 本文行情数据服务由 TickDB.ai 提供。

  • 文档:https://docs.tickdb.ai
  • MCP:https://mcp.tickdb.ai

本文仅讨论技术接入和工具配置方式,不构成任何投资建议。

相关推荐
Cosolar1 小时前
从零搭建本地 RAG 系统:LangChain + LM Studio 完整实战指南
人工智能·后端·面试
ting94520001 小时前
Fere AI 技术深度解析:面向加密货币与预测市场的自主交易智能体架构
人工智能·架构
Yeats_Liao2 小时前
物联网接入层技术剖析(四):当epoll遇见MQTT
java·linux·服务器·网络·物联网·架构
mCell2 小时前
可观测性实战:Prometheus + Grafana 全栈监控
运维·后端·google
彭于晏Yan2 小时前
TransmittableThreadLocal原理及作用
spring boot·后端
Swift社区2 小时前
AI + 鸿蒙 App:下一代应用架构
人工智能·架构·harmonyos
彭于晏Yan2 小时前
OkHttp 与 RestTemplate 技术选型对比
java·spring boot·后端·okhttp
woniu_buhui_fei2 小时前
工作中常用的注解梳理
后端
豆豆2 小时前
WordPress与PageAdmin CMS深度技术对比:从架构到国产化合规的全维度分析
架构·cms·网站建设·建站系统·内容管理系统·网站管理系统·站群cms