综合

智谱GLM-4 接金融数据:工具描述多写三个字,模型少犯一类错

作者: TickDB Research · 发布: 2026/5/31 · 阅读: 2

标签: C 类, 知乎, AI 工具

你把智谱开放平台的 Function Calling 示例从“查天气”改成“查行情”。在多次测试中,GLM-4-Flash 不仅调对了工具,还能把用户说的“茅台”映射成 600519.SH——这是中文模型在 A 股数据上的天然优势。但当三个行情工具同时注册时,同一个 query 在第三轮对话里突然调错了函数。优势区和暗坑区,只隔着一套工具描述的写法。

一、GLM-4 的中文红利,在金融场景下是双刃剑

GLM-4 对中文金融术语的理解、对 A 股代码的熟悉程度,明显优于仅靠英文语料做中文适配的模型。你用中文写“查询贵州茅台和宁德时代的最新股价”,它不仅能提取出两只股票,还可能自动补全代码——这是训练语料里 A 股信息密度高的直接体现。

但这层中文红利同时也是隐性债务。

GLM-4 在金融数据场景下的表现优势面暗坑面
中文名称到代码的映射A 股常见股票可自动映射(开发者经验观察,非官方保证)港股、美股的映射准确率递减;遇到“苹果”这类歧义词可能选错市场
中文工具描述解析对“最新价”“成交量”等术语理解准确两个工具的中文描述高度相似时,选择置信度下降
免费额度零成本验证,适合原型开发免费版在多工具场景下的稳定性可能弱于付费版本(以智谱官网当前文档为准)
错误信息的中文理解能准确理解“请求超限”“Key失效”等提示可能“过度理解”——从“请求超限”推断出“服务端繁忙,建议稍后重试”而不是只告知失败

这些特点决定了一件事:用 GLM-4 接金融数据,你需要的不是“把 curl 改成 Python”,而是一套专门针对中文模型行为特征的工具设计方法。

为了让模型的优势不被数据源的不一致性抵消,本文以 TickDB 的统一行情接口作为数据后端——跨市场一致的字段命名、错误码语义和 symbol 格式,让你给 GLM-4 定义的工具只需一套字段映射和一套错误处理逻辑。如果你在 AI 编码环境中工作,TickDB 也提供 MCP 端点 mcp.tickdb.ai,可在 Claude Code、Cursor 等工具中直接调用,无需手写 HTTP 封装。

二、三个设计决策,决定你的 Agent 会不会“自作主张”

决策一:工具描述的第一句话,写功能还是写边界?

GLM-4 的 tool_choice 默认为 auto。模型根据 function.description 判断“这个工具能做什么”。当一个工具时,任何合理描述都能工作;当三个工具共存时,描述里的每一个模糊词都是误选的种子。

金融场景最小工具集通常包括:

工具功能如果描述写成这样,模型会混淆
get_ticker实时行情快照“获取股票行情数据”
get_kline历史 K 线“获取股票历史数据”
get_kline_latest最近一根 K 线“获取最新 K 线数据”

当三个描述分别是“获取行情”“获取历史”“获取最新K线”时,GLM-4 对“现在价格是多少”可能选 get_ticker,也可能选 get_kline_latest——因为两者都能回答“现在价格”。而 get_kline_latest 返回的是最近一根已完成 K 线的收盘价,不是实时价。

正确的写法是把“不要用”写进 description 的第一句

# ✅ GLM-4 能正确区分
"description": (
    "获取品种实时行情快照(最新价、24小时成交量)。"
    "不要使用此函数获取历史K线数据——历史K线应使用 get_kline。"
    "不要使用此函数获取最近K线——最近K线应使用 get_kline_latest。"
)

对于 GLM-4,在 description 中使用中文否定句划定工具边界,是一种在实践中被验证有效的做法(开发者经验观察)。模型不是理解不了“获取行情”和“获取K线”的区别——它是不确定在当前上下文里你更想要哪个。你把“不要用这个”写在第一句,它就不再犹豫。

决策二:错误返回让模型自行理解,还是写死它的行为?

金融数据接口的返回不是成功/失败二元。get_ticker 可能遇到限流、Key 失效、symbol 不存在等情况。如果你把 API 返回的 raw JSON 直接丢给模型,GLM-4 的中文理解能力反而会成为风险:

实际状态如果你把 raw JSON 直接丢给模型
限流 3001模型可能回复“请求频率过高,建议稍后重试,根据最近一次查询……”然后编一个价格
Key 无效 1001模型可能回复“API Key 配置有误,请检查。在此期间,根据公开信息……”
symbol 不存在模型可能回复“该股票代码不存在,您是否想查询……”然后推荐一个代码

这不是 GLM-4 的 bug——它只是太想帮忙了。但在金融场景下,任何“帮忙”都是不可接受的。

解决方式:在工具函数内部,把所有错误转换为模型可以直接判断的 success 字段,并在 system prompt 中写死它的行为。

# 无论什么错误,始终返回此结构
{"success": False, "data": [], "error_code": "RATE_LIMITED"}
# system prompt 中的硬规则——不给模型留任何自行发挥的空间
工具返回 success=false 时,你只能说:
"当前无法获取行情数据,错误原因:<error_code>。"
不要说"建议稍后重试"、不要猜测原因、不要提供替代方案、绝对不要编造任何数值。

这条规则在 GLM-4 上需要比在其他模型上写得更“绝”——它的中文能力越强,就越容易从 error_code 里读出额外意思,然后自己扩展回复。你给它留一条缝,它就敢钻进去。

决策三:利用中文映射优势,还是禁止它?

GLM-4 能从“茅台”自动提取 600519.SH,从“宁德时代”提取 300750.SZ——这是训练语料给的天然能力。但这个能力有三个边界:

  1. A 股覆盖率好,港股美股递减:“腾讯”映射 700.HK 成功率较高,“苹果”映射 AAPL.US 成功率也较高,但“苹果”有歧义——模型需要判断是 AAPL 还是水果。
  2. 映射结果的 symbol 格式可能不标准:模型可能返回 600519.SSE 而非 600519.SH.SSE 在 TickDB 不是有效后缀,查询静默失败。
  3. 你可以通过 system prompt 完全关闭这个行为——GLM-4 的指令跟随能力足够强,只要 prompt 里明确写了“不要自行转换中文名称为代码”,它就会照做。

建议策略:原型验证阶段利用自动映射快速跑通流程。生产环境中通过 system prompt 关闭这个行为,改由专门的映射函数(对接 TickDB 的 GET /v1/symbols/available 品种列表)完成中文名称到标准代码的转换。这样 symbol 格式完全可控。

三、完整实现

以下代码将上述三个决策落实为三层封装:参数校验层在请求前拦截非法格式,工具执行层统一限流退避和结构化错误返回,GLM-4 对话层管理完整的 Function Calling 生命周期。

SDK 调用约定提示ZhipuAI 类名、chat.completions.create 方法签名、tool_choice="auto"role: "tool" 回填格式及 glm-4-flash 模型名称,均以智谱开放平台官网当前文档为准。正式接入前建议核对你所使用的 SDK 版本的实际行为。

环境准备

pip install zhipuai python-dotenv requests

.env 文件(不要提交到版本控制):

ZHIPUAI_API_KEY=你的智谱API Key
TICKDB_API_KEY=你的TickDB API Key
TICKDB_REST_URL=https://api.tickdb.ai

完整代码:glm4_tickdb.py

# glm4_tickdb.py
# 智谱 GLM-4 Function Calling 接入 TickDB 实时行情
# 三层封装:参数校验 → 工具执行与错误处理 → GLM-4 对话
# Python 3.10+ 可直接运行

import os
import json
import time
import re
import requests
from decimal import Decimal, InvalidOperation
from dotenv import load_dotenv
from zhipuai import ZhipuAI

load_dotenv()

# ================== 配置 ==================
ZHIPUAI_API_KEY = os.getenv("ZHIPUAI_API_KEY")
TICKDB_API_KEY = os.getenv("TICKDB_API_KEY")
TICKDB_REST_URL = os.getenv("TICKDB_REST_URL", "https://api.tickdb.ai")
MAX_RETRIES = 3

# ================== 第一层:Symbol 格式校验 ==================
SYMBOL_PATTERN = re.compile(
    r'^(\d{6}\.(SH|SZ|BJ)|[1-9]\d{0,4}\.HK|[A-Z]{1,5}\.US|'
    r'[A-Z]{2,4}[A-Z0-9]{2,8}|[A-Z]{2}\d{4})




)

def validate_symbols(symbols_str: str) -> list:
    """返回不合法的 symbol 列表"""
    if not symbols_str or not symbols_str.strip():
        return []
    sym_list = [s.strip() for s in symbols_str.split(",") if s.strip()]
    return [s for s in sym_list if not SYMBOL_PATTERN.match(s)]


# ================== 第二层:TickDB 工具执行 ==================
def get_ticker(symbols_str: str, retry_count: int = 0) -> dict:
    """
    获取实时行情快照。
    不要使用此函数获取历史K线——历史K线应使用 get_kline。

    始终返回 {"success": bool, "data": [...], "error_code": str}
    """
    invalid = validate_symbols(symbols_str)
    if invalid:
        return {"success": False, "data": [],
                "error_code": f"INVALID_SYMBOL_FORMAT: {invalid}"}

    if retry_count > MAX_RETRIES:
        return {"success": False, "data": [], "error_code": "MAX_RETRIES_EXCEEDED"}

    headers = {"X-API-Key": TICKDB_API_KEY}
    params = {"symbols": symbols_str}

    try:
        resp = requests.get(
            f"{TICKDB_REST_URL}/v1/market/ticker",
            headers=headers, params=params, timeout=10
        )
    except requests.exceptions.Timeout:
        return {"success": False, "data": [], "error_code": "TIMEOUT"}
    except requests.exceptions.ConnectionError:
        return {"success": False, "data": [], "error_code": "CONNECTION_ERROR"}

    data = resp.json()

    # 限流:解析 Retry-After,保护非整数
    if resp.status_code == 429 or data.get("code") == 3001:
        retry_after = resp.headers.get("Retry-After", "5")
        try:
            wait_seconds = float(retry_after)
        except (ValueError, TypeError):
            wait_seconds = 5
        time.sleep(wait_seconds)
        return get_ticker(symbols_str, retry_count + 1)

    # 鉴权错误(阻断,不重试)
    if data.get("code") == 1001:
        return {"success": False, "data": [], "error_code": "INVALID_API_KEY"}
    if data.get("code") == 1002:
        return {"success": False, "data": [], "error_code": "MISSING_API_KEY"}
    if data.get("code") == 1004:
        return {"success": False, "data": [], "error_code": "PERMISSION_DENIED"}
    if data.get("code") == 2002:
        return {"success": False, "data": [], "error_code": "SYMBOL_NOT_FOUND"}
    if data.get("code") != 0:
        return {"success": False, "data": [],
                "error_code": f"API_ERROR_{data['code']}"}

    # 字段映射
    results = []
    for d in data.get("data", []):
        try:
            price = Decimal(str(d["last_price"]))
            vol = Decimal(str(d.get("volume_24h", "0")))
            ts = d["timestamp"]
        except (InvalidOperation, ValueError):
            continue
        results.append({
            "symbol": d["symbol"],
            "last_price": str(price),
            "volume_24h": str(vol),
            "timestamp_ms": ts,
            "timestamp_note": "毫秒UTC (ticker接口)"
        })

    if not results:
        return {"success": False, "data": [], "error_code": "EMPTY_DATA"}
    return {"success": True, "data": results, "error_code": ""}


# ================== GLM-4 工具定义 ==================
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_ticker",
            "description": (
                "获取品种实时行情快照(最新价、24小时成交量、毫秒UTC时间戳)。"
                "不要使用此函数获取历史K线数据——历史K线应使用 get_kline 函数。"
                "不要使用此函数获取盘口深度——盘口深度应使用 get_order_book 函数。"
                "不要使用此函数获取最近K线——最近K线应使用 get_kline_latest 函数。"
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "symbols": {
                        "type": "string",
                        "description": (
                            "逗号分隔的品种代码,例如 '600519.SH,700.HK,AAPL.US'。"
                            "A 股格式为代码.SH/SZ/BJ(如 600519.SH),"
                            "港股为代码.HK 无前导零(如 700.HK),"
                            "美股为代码.US(如 AAPL.US),"
                            "期货为代码本身无后缀(如 IF2606)。"
                        )
                    }
                },
                "required": ["symbols"]
            }
        }
    }
]

SYSTEM_PROMPT = """你是一个严谨的金融数据助手。你只能通过 get_ticker 工具获取实时行情。

=== 工具使用规则 ===
1. 工具返回 success=true 时,基于 data 中的结构化数据回答用户问题。
2. 工具返回 success=false 时,只允许回复以下固定句式:
   "当前无法获取行情数据,错误原因:<error_code>。"
   不允许添加"建议稍后重试"、"请检查网络"、"数据源可能不可用"等任何扩展说明。
   绝对不要根据 error_code 推测或编造任何价格、成交量或数值。

=== Symbol 规则 ===
3. A 股格式: 6位数字.SH/.SZ/.BJ | 港股: 数字.HK (无前导零) | 美股: 字母.US | 期货: 无后缀
4. 用户用中文名称(如"茅台")查询时,回复:
   "请提供标准品种代码,例如 600519.SH(贵州茅台)、700.HK(腾讯控股)、AAPL.US(苹果)。"
   绝对不要自行将中文名称转换为代码。"""


# ================== 第三层:GLM-4 对话管理 ==================
def chat_with_tools(user_query: str, model: str = "glm-4-flash") -> str:
    """单轮带工具调用的金融查询"""
    client = ZhipuAI(api_key=ZHIPUAI_API_KEY)
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": user_query}
    ]

    response = client.chat.completions.create(
        model=model,
        messages=messages,
        tools=TOOLS,
        tool_choice="auto"
    )
    assistant_message = response.choices[0].message

    if not assistant_message.tool_calls:
        return assistant_message.content or ""

    messages.append(assistant_message)
    for tool_call in assistant_message.tool_calls:
        if tool_call.function.name == "get_ticker":
            try:
                args = json.loads(tool_call.function.arguments)
            except json.JSONDecodeError:
                tool_result = {"success": False, "data": [],
                               "error_code": "INVALID_ARGUMENTS_JSON"}
            else:
                tool_result = get_ticker(args.get("symbols", ""))

            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(tool_result, ensure_ascii=False)
            })

    final_response = client.chat.completions.create(
        model=model,
        messages=messages
    )
    return final_response.choices[0].message.content


# ================== 测试入口 ==================
if __name__ == "__main__":
    print("=== 测试1: 跨市场标准代码查询 ===")
    print(chat_with_tools("查一下 600519.SH、700.HK 和 AAPL.US 的最新价格"))

    print("\n=== 测试2: 中文名称输入(应引导而非映射) ===")
    print(chat_with_tools("茅台现在多少钱?"))

    print("\n=== 测试3: 错误 symbol 格式 ===")
    print(chat_with_tools("查询 600519.SSE 的价格"))

    print("\n=== 测试4: 不存在的 symbol ===")
    print(chat_with_tools("查询 NOTEXIST.SH 的价格"))

关键解读

  • 工具描述description 开头写功能,紧接着写三个“不要用”——不是可有可无的注释,而是直接决定模型选择精度的硬配置。
  • 错误返回:无论底层 API 发生了什么,始终返回带 success 字段的同一结构字典。模型的回复行为由这个字段触发,不由它自己判断。
  • system prompt:对 success=false 规定了固定回复句式,并明文禁止“建议稍后重试”“请检查网络”等扩展——这是针对 GLM-4 “过度理解”倾向的定向约束。
  • 模型版本glm-4-flash 适合原型验证。多工具(≥3)生产场景,付费版本的工具选择精度通常更稳定。

四、统一数据层在 GLM-4 场景下的价值

当多个金融接口的字段名不统一时,你每接入一个新数据源,GLM-4 的工具函数都要重写一套字段映射和错误处理逻辑。一个统一的数据接口可以把这个复杂度压到最低。

工程困境统一接口应提供什么TickDB 的实现
模型分不清不同接口的成交量字段字段语义按端点严格隔离ticker 用 volume_24h,kline 用 volume,不混用
限流后模型“过度理解”错误信息标准错误码 + Retry-After3001 携带建议等待时间,客户端据此退避
中文名称映射格式不统一品种列表接口 + 统一后缀规范GET /v1/symbols/available 可查正版 symbol
Function Calling 切换到 AI 编码助手场景MCP 端点,无需手写 HTTP 代码mcp.tickdb.ai 可在 Claude Code、Cursor 等工具中直接调用

五、结尾

给 GLM-4 接上金融数据,代码层面只是注册了一个 get_ticker 函数。但让它不犯错,靠的是三个在通用教程里不会展开的东西:描述里那三句“不要用”,返回结构里那个 success 字段,system prompt 里那条把模型的嘴焊死的硬规则。

一个反直觉的观察:GLM-4 对中文的理解越强,它在金融场景下的风险反而越大——因为强到它觉得自己可以在数据缺失时“帮用户一把”。而金融场景下,任何“帮一把”都是不能接受的。你把约束写在 tool description 里,它照做;你不写,它默认帮你——这就是中文模型的纪律边界。

你在用国产模型接入业务数据时,有没有遇到过模型“过度帮忙”的情况?是在 prompt 里堵,还是在代码里拦?欢迎在评论区聊聊你的防御策略。


📡 数据示例由 TickDB.ai 提供


标签:智谱 GLM / Function Calling / 金融数据接入 / 工具调用 / TickDB / GLM-4


文末自检清单

我要查什么怎么查
每个工具的 description 第一句是否写了“不要用 X,应选 Y”?打开 TOOLS 定义,逐条检查 description 开头
get_ticker 所有 return 分支是否都包含 success 字段?检查函数体内每个 return 语句
system prompt 里是否明确禁止模型在失败时扩展说明?搜索 prompt 中“固定句式”和“不允许添加”
成交量字段用的是 int() 还是 Decimal()搜索 int(volume,有则改
限流退避是否解析了 Retry-After检查 3001 分支中是否调用了 resp.headers.get("Retry-After")

| 中文名称映射是由模型还是代码完成? | 如果 prompt 写了“不要自行转换”,确认代码里有替代映射函数 |

通过 TickDB API 获取实时行情数据

一个 API 接入外汇、加密货币、美股、港股、A股、贵金属和全球指数的实时行情。支持 WebSocket 低延迟推送,免费开始使用。

免费领取 API Key查看 API 文档

相关文章