目录
- [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_kline的symbol描述中明确"只能传入一个品种代码,不支持逗号分隔"。 limit增加数值约束 :minimum: 1、maximum: 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%
这个轨迹清晰展示了四步循环:user → assistant(tool_calls) → tool → assistant(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 模式的关键规则:
additionalProperties必须设为False- 所有
properties中定义的字段,必须出现在required数组中 - 不能使用
oneOf、anyOf等不支持的类型
常见 Strict 模式错误
如果你把普通模式的 schema 直接加 strict: true,会触发服务端 400 错误。最典型的例子是 get_kline 的 limit 字段:
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 列表会累积 user、assistant、tool 三种角色的消息。下一轮调用时,模型能从完整的历史消息中解析出之前查询的品种、数据内容以及指代关系。
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 捕获,返回原始参数和错误信息 |
工程化建议
-
tools 定义与执行函数分离:tools 定义放 JSON 文件,执行函数放独立模块。新增工具时改配置不碰代码。
-
tool_call_id精确关联:每个 tool_call 有唯一 ID,返回结果时必须带上,模型靠这个 ID 关联请求和结果。 -
对话历史长度控制:多轮对话会导致 messages 超出上下文限制。保留最近 20 轮,早期消息做摘要压缩。
-
价格计算用 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_pricekline端点 → 价格字段为open/high/low/close- 两者不可跨端点混用------从 K 线数据里取
last_price会报 KeyError,从 ticker 里取close同理。
你在用 DeepSeek 接实时数据时,遇到过模型选错工具或编造数据的情况吗?评论区聊。
📡 本文行情数据服务由 TickDB.ai 提供。
- 文档:
https://docs.tickdb.ai - MCP:
https://mcp.tickdb.ai
本文仅讨论技术接入和工具配置方式,不构成任何投资建议。