文档目录
函数调用
通过把函数名和闭合取值集合的参数映射到带置信度的 TypeSafe 问题,把自然语言的交易请求转为对普通带类型函数的调用。
非官方中文翻译 · 内容以 英文原文 ↗ 为准 · 同步于 2026-10-15
当你点一杯「大杯冰燕麦拿铁,不加糖」时,咖啡师不会把你的原话写下来。他们会在杯子上勾选四个选项。这个实战示例对交易 API 做的是同样的事:一句话进去,出来的是一个函数名及其参数,参数呈现为已判定的枚举值,每个都附带置信度。
"plot rolling correlation between nvda and spy for the past month"
rolling_correlation(symbol='NVDA', benchmark='SPY', window='1mo') confidence 0.91
"compare nvda amd and msft over the past three months"
compare_returns(symbols=['NVDA', 'AMD', 'MSFT'], window='3mo') confidence 0.94
"show me apple daily with volume"
plot_price(symbol='AAPL', resolution='1d', include_volume=True) confidence 0.75
"what tickers do you have"
list_symbols() confidence 1.00这些调用会送往交易助手里的十个普通函数。它们的参数值取自固定列表,因此本身就是 Literal:
def plot_price(
symbol: Literal["SPY", "NVDA", "AMD", "AAPL", "MSFT", "TSLA"],
style: Literal["line", "candles"] = "line",
resolution: Literal["1m", "5m", "15m", "1h", "1d"] = "15m",
window: Literal["1d", "1w", "1mo", "3mo"] = "1w",
include_volume: bool = False,
moving_average: Literal["9", "20", "50"] | None = None,
log_scale: bool = False,
): ...参数取值来自固定列表的就是闭合取值集合。当它从列表中取一个值时,会得到一个 Choice 问题,备选项恰好就是那些值,因此无论送入函数的是什么值,函数都能接受。你不需要修改函数。要添加的是一份 spec,用通俗的话说明每个参数的含义。最终你会得到一个 Dispatcher,可以指向你自己的函数。
设置环境
pip install ipython polars matplotlib numpy 'cooksafe>=0.2.0,<0.3.0'设置 TYPESAFE_API_KEY。在这个文件旁边有两个模块。trader.py 包含那十个函数,以及一个 TypeSafe 客户端,它从缓存中读取答案,所以重新渲染时会重放下面的数字,而不会调用 API。dispatch.py 包含读取 signature 和 spec 并发起调用的代码。
import json
from pathlib import Path
from cooksafe import make_playground_link
from dispatch import ROUTE, Dispatcher, closed_sets
from IPython.display import Markdown, display
from trader import TOOLS, client, load
TYPESAFE_MODEL = "jev-1.12"
print(f"{len(TOOLS)} functions over {load().height:,} one-minute bars")10 functions over 156,780 one-minute bars在签名中查找闭合取值集合
类型注解已经说明了哪些参数来自固定列表,以及每个列表中有哪些值。closed_sets 读取签名,并将这些参数归为三种形态:choice(一个 Literal,即从列表中取一个值)、set(一个 list[Literal[...]],即从中取任意多个)、flag(一个 bool,即开或关)。所有十个函数都在 trader.py 中定义。
for name, fn in TOOLS.items():
shapes = closed_sets(fn)
print(
f" {name:<20}{len(shapes)} "
+ ", ".join(f"{a}:{s}" for a, (s, _) in shapes.items())
)
print(
f"\n{sum(len(closed_sets(fn)) for fn in TOOLS.values())} fillable arguments in total"
) list_symbols 0
market_summary 1 window:choice
plot_price 7 symbol:choice, style:choice, resolution:choice, window:choice, include_volume:flag, moving_average:choice, log_scale:flag
intraday_pattern 3 symbol:choice, window:choice, metric:choice
compare_returns 3 symbols:set, window:choice, normalize:flag
rolling_correlation 4 symbol:choice, benchmark:choice, window:choice, resolution:choice
summary_stats 2 symbol:choice, window:choice
volatility 3 symbol:choice, window:choice, annualized:flag
top_movers 2 window:choice, direction:choice
drawdown 3 symbol:choice, window:choice, plot:flag
28 fillable arguments in totaltop_movers 展示了哪些会被略去。它的三个参数中有两个是闭合取值集合。第三个 limit 是 int,因此永远不会产生问题,保留其默认值 3。自由文本、数字和日期同理,不会产生问题,函数的默认值生效。
编写 spec
Literal 只给出字符串 "1mo" 和 "3mo"。它不会告诉你用户输入「this quarter」时指的是后者。这是 spec 要说明的。每个参数对应一个问题,每个选项对应一行描述,每个函数对应一段说明,外加一个问题用于在函数之间做选择。spec 存放在 spec.json 中,也可以让一个大语言模型(LLM)根据签名自动生成。
SPEC = json.loads(Path("spec.json").read_text())
for argument in ("style", "moving_average"):
print(
json.dumps(
{argument: SPEC["functions"]["plot_price"]["arguments"][argument]}, indent=2
)
){
"style": {
"question": "Does the user want a plain line or candles?",
"stated": "Does the user say how the chart should be drawn, such as a line, candles, or OHLC bars?",
"options": {
"line": "a simple line through the closing prices",
"candles": "a candlestick or OHLC chart, showing each bar's open, high, low and close"
}
}
}
{
"moving_average": {
"question": "How many bars should the moving average cover - nine, twenty, or fifty?",
"stated": "Does the user ask for a moving average or a smoothed line over the candles?",
"options": {
"9": "a nine-bar moving average, a fast one",
"20": "a twenty-bar moving average",
"50": "a fifty-bar moving average, a slow one"
}
}
}选项的键就是函数接受的字符串,因此之后不需要把标签再映射回参数。stated 把一个参数变为可选。它是第二个是/否问题,用来判断这条命令是否提到了该参数。答案为否时,调用会省略这个参数,函数自身的默认值生效。
一个 set 类型的参数会按成员逐个产生问题,{} 用作成员名的占位符。"Does the user want {} in the comparison?" 会按 ticker 数量变成多个问题。
编写每个问题时,应围绕概念本身,而不是用户可能使用的措辞,因为匹配是按含义进行的:「is amd tracking nvidia lately」能够命中 rolling_correlation,尽管 spec.json 中既没有 tracking 也没有 lately。避免用参数名来命名问题 —— "Which resolution?" 让命令没有任何可匹配的线索。
把 spec 转化为问题
Dispatcher 根据 spec 一次性构建出问题。之后每条命令就是一个请求,携带函数的选择以及每个函数的参数,dispatcher 只读取被选中函数的答案。
assistant = Dispatcher(SPEC, TOOLS, client)
print(f"{len(assistant.questions)} questions per command, among them:")
for qid in (
"__tool__",
"plot_price.style",
"plot_price.style?",
"compare_returns.symbols.NVDA",
):
question = assistant.questions[qid]
print(f" {qid:<30}{question['type']:<8}{str(question['instructions'])[:64]}")54 questions per command, among them:
__tool__ choice What is the user asking the trading assistant to do?
plot_price.style choice Does the user want a plain line or candles?
plot_price.style? noul Does the user say how the chart should be drawn, such as a line,
compare_returns.symbols.NVDA noul Does the user want NVDA in the comparison?运行十四条命令
一个请求占一行,其 confidence 是这次调用背后最不确定的那个判断。
COMMANDS = [
"show nvda 1h",
"plot rolling correlation between nvda and spy for the past month",
"when during the day does nvda trade the most",
"what moved today",
"what tickers do you have",
"how did the market do this week",
"candles for tesla with a 20 period moving average",
"compare nvda amd and msft over the past three months",
"how volatile is tsla",
"biggest losers today",
"worst drawdown for nvda this quarter, and chart it please",
"spy stats for the last month",
"show me apple daily with volume",
"is amd tracking nvidia lately",
]
CALLS = {command: assistant(command) for command in COMMANDS}
for command, call in CALLS.items():
print(f' "{command}"')
print(
f" {str(call):<66}confidence {call.confidence:.2f}"
f" tool {call.tool.probability:.2f}"
) "show nvda 1h"
plot_price(symbol='NVDA', resolution='1h') confidence 0.78 tool 1.00
"plot rolling correlation between nvda and spy for the past month"
rolling_correlation(symbol='NVDA', benchmark='SPY', window='1mo') confidence 0.91 tool 1.00
"when during the day does nvda trade the most"
intraday_pattern(symbol='NVDA') confidence 0.53 tool 1.00
"what moved today"
top_movers(window='1d', direction='gainers') confidence 0.90 tool 0.90
"what tickers do you have"
list_symbols() confidence 1.00 tool 1.00
"how did the market do this week"
market_summary(window='1w') confidence 0.96 tool 0.99
"candles for tesla with a 20 period moving average"
plot_price(symbol='TSLA', style='candles', moving_average='20') confidence 0.69 tool 0.97
"compare nvda amd and msft over the past three months"
compare_returns(symbols=['NVDA', 'AMD', 'MSFT'], window='3mo') confidence 0.94 tool 1.00
"how volatile is tsla"
volatility(symbol='TSLA') confidence 0.96 tool 1.00
"biggest losers today"
top_movers(window='1d', direction='losers') confidence 0.98 tool 0.98
"worst drawdown for nvda this quarter, and chart it please"
drawdown(symbol='NVDA', window='3mo', plot=True) confidence 0.84 tool 0.84
"spy stats for the last month"
summary_stats(symbol='SPY', window='1mo') confidence 0.88 tool 0.88
"show me apple daily with volume"
plot_price(symbol='AAPL', resolution='1d', include_volume=True) confidence 0.75 tool 0.85
"is amd tracking nvidia lately"
rolling_correlation(symbol='AMD', benchmark='NVDA') confidence 0.82 tool 0.82两条长命令都按要求得出了结果。「plot rolling correlation between nvda and spy for the past month」从一句话中填出了四个参数。其中两个参数 symbol 和 benchmark 都从同样的六个 ticker 中取值,每个 ticker 都落到了正确的参数中,因为问题清楚说明了角色:被度量的那个、最先被提到的 对应 第二个被提到的、作为标尺的那个。「compare nvda amd and msft over the past three months」把三个 ticker 放入了集合,其余三个则被排除在外。
运行其中三条:
for command in (
"plot rolling correlation between nvda and spy for the past month",
"compare nvda amd and msft over the past three months",
"when during the day does nvda trade the most",
):
print(f'"{command}" -> {CALLS[command]}')
display(CALLS[command].run())"plot rolling correlation between nvda and spy for the past month" -> rolling_correlation(symbol='NVDA', benchmark='SPY', window='1mo')
"compare nvda amd and msft over the past three months" -> compare_returns(symbols=['NVDA', 'AMD', 'MSFT'], window='3mo')
"when during the day does nvda trade the most" -> intraday_pattern(symbol='NVDA')
再看以文本回答的那些:
for command in ("how did the market do this week", "biggest losers today"):
print(f'"{command}" -> {CALLS[command]}')
print(CALLS[command].run(), "\n")"how did the market do this week" -> market_summary(window='1w')
the board over 1w
NVDA 254.12 9.62% 389,465,563
AMD 184.20 1.51% 182,740,497
AAPL 258.71 0.97% 223,818,998
SPY 664.86 0.40% 138,617,365
MSFT 451.35 0.26% 113,427,173
TSLA 320.22 -0.97% 266,317,023
"biggest losers today" -> top_movers(window='1d', direction='losers')
top 3 losers over 1d
AMD -0.57% -> 184.20
MSFT 0.67% -> 451.35
AAPL 1.40% -> 258.71 读取置信度
confidence(置信度)报告的是本次调用中置信度最低的那一项判断,而不是所有判断置信度的乘积,因为只要有一个参数出错,整个结果就会被推翻。乘积回答的是另一个问题(「是否每一部分都对」),它会随着函数接收的参数增多而下降,无论其中任何一次判断是否薄弱。
下面按参数逐个说明这个数字的来源:
call = CALLS["is amd tracking nvidia lately"]
print(f'"is amd tracking nvidia lately" -> {call} confidence {call.confidence:.2f}')
for name, argument in call.arguments.items():
top = sorted(argument.distribution.items(), key=lambda kv: -kv[1])[:3]
shown = "omitted, default stands" if argument.omitted else repr(argument.value)
print(
f" {name:<12}{shown:<26}p {argument.probability:.2f} "
+ " ".join(f"{k} {v:.2f}" for k, v in top)
)
print(f" weakest argument: {call.weakest().name}")"is amd tracking nvidia lately" -> rolling_correlation(symbol='AMD', benchmark='NVDA') confidence 0.82
symbol 'AMD' p 0.87 AMD 0.87 NVDA 0.13 AAPL 0.00
benchmark 'NVDA' p 0.78 NVDA 0.92 AMD 0.08 AAPL 0.00
window omitted, default stands p 0.96
resolution omitted, default stands p 0.99
weakest argument: benchmarkwindow 和 resolution 在这里都被省略了,因为「lately」既没说清楚要回溯多远,也没说基于什么粒度的 K 线,所以 rolling_correlation 直接使用其默认值,即一个月和小时级 K 线。这正是 stated 这一题的作用。如果没有它,选择题就必须为 window 指定一个具体值,而且会很有把握地给出某个值。
在 playground 中打开它
下面的链接保存了一条指令,以及为它所选中的函数所对应的问题:从十段函数描述中做出的选择题,以及 rolling_correlation 的四个参数问题。在那里编辑指令,参数也会随之变化。
COMMAND = "plot rolling correlation between nvda and spy for the past month"
picked = CALLS[COMMAND]
playground_link = make_playground_link(
COMMAND,
{ROUTE: assistant.questions[ROUTE]}
| {q: v for q, v in assistant.questions.items() if q.startswith(f"{picked.name}.")},
models=[TYPESAFE_MODEL],
)
display(
Markdown(
f"🔗 [Open the command and its questions in the TypeSafe playground]({playground_link})"
)
)在 TypeSafe playground 中打开该指令及其问题 →
本文档由 Mintlify 构建和托管,它是一个面向开发者的文档平台。