把指令转成函数调用
一个笔记应用有命令栏。用户会输入 "send this to the design team but they shouldn't be able to change anything" 这样的话,应用应当调用 share_note(permission="view")。Valen 负责选择函数和每个封闭集合参数;代码维护函数表,决定何时追问,并执行调用。
工作原理
代码里的函数表是所有问题的唯一来源。一次请求包含:
| 问题 | 类型 | 何时读取 |
|---|---|---|
function:用户想让应用做什么? | 在四个函数中做选择题 | 始终读取 |
is_command:笔记应用能否完成用户的请求? | predicate | 始终读取 |
每个函数的每个参数对应一个 <function>.<arg> | 选择题 | 只读取被选中的函数 |
预先问出所有函数的参数,就是推测式并行提问:问题开销很小,并且基于同一输入一起运行,所以无论最后选中哪个函数,一次往返就够了。选择题总会选出一个选项,因此“不是指令”的情况单独用一道判断题来问,而不是加一个 none 选项。
is_command | 函数选择题 | 动作 |
|---|---|---|
| 低于 0.3 | 任意 | not_a_command |
| 0.3 – 0.7 | 不确定 | show_help:列出应用能做的事 |
| 0.7 及以上 | 不确定 | clarify:在前两名函数之间追问 |
| 任意 | 最高 ≥ 0.8 且差距 ≥ 0.2 | call;某个参数不确定时为 clarify_arguments |
运行
先下载一次示例包。caches/function_calling.json 中录制的回答让脚本无需 API key 即可运行;加 --replay off 则改为调用免费托管 API。
Bash
curl -fLO https://valen-team.github.io/cookbook/valen-cookbook.zip
python -m zipfile -e valen-cookbook.zip .
cd valen-cookbook && python -m pip install -r requirements.txt
python function_calling.py
python function_calling.py --replay off --base-url https://api.valen-team.workers.dev/v1
代码
function_calling.py
"""Turn a command into a function call: one request picks the function and every closed-set argument. Apache-2.0."""
import json
from common import HERE, choice_status, client, emit, parser, request
# Each function's closed-set arguments. Free text, numbers and dates stay with code.
TOOLS = {
"set_reminder": {"description": "Remind the user about this note later",
"args": {"when": {"later_today": "Later today", "tomorrow": "Tomorrow",
"next_week": "Some time next week"}}},
"share_note": {"description": "Send or share this note with other people",
"args": {"permission": {"view": "They can read but not change it",
"edit": "They can change it"}}},
"change_theme": {"description": "Change how the app looks",
"args": {"theme": {"light": "Light colors", "dark": "Dark colors",
"system": "Follow the device setting"}}},
"export_note": {"description": "Save this note as a file",
"args": {"format": {"pdf": "PDF document", "markdown": "Markdown text file",
"docx": "Word document"}}},
}
def questions():
asked = [{"name": "function", "type": "choice", "instructions": "What does the user want the notes app to do?",
"choices": [{"value": name, "description": tool["description"]} for name, tool in TOOLS.items()]},
# A choice always picks a function, so a separate predicate tells code when none applies.
{"name": "is_command", "type": "predicate",
"instructions": "A notes app could do what the user asks, without outside information such as weather or news."}]
# Speculative: every function's arguments are asked now; code reads only the chosen function's.
for name, tool in TOOLS.items():
for arg, options in tool["args"].items():
asked.append({"name": f"{name}.{arg}", "type": "choice",
"instructions": f"If the user wants to {tool['description'].lower()}, which {arg} do they mean?",
"choices": [{"value": value, "description": text} for value, text in options.items()]})
return asked
def run(predict, command):
answers = request(predict, command, questions())
function = answers["function"]
confidence, certain = choice_status(function)
is_command = answers["is_command"]["probability"]
base = {"command": command, "is_command": is_command, "confidence": confidence}
if is_command < 0.3:
return {**base, "status": "not_a_command"}
if not certain:
if is_command < 0.7:
return {**base, "status": "show_help"}
top = sorted(function["probabilities"], key=lambda x: x["probability"], reverse=True)[:2]
return {**base, "status": "clarify", "options": [x["value"] for x in top]}
kwargs, unsure = {}, []
for arg in TOOLS[function["choice"]]["args"]:
answer = answers[f"{function['choice']}.{arg}"]
kwargs[arg] = answer["choice"]
if not choice_status(answer)[1]:
unsure.append(arg)
return {**base, "status": "clarify_arguments" if unsure else "call",
"call": {"function": function["choice"], "kwargs": kwargs}, "unsure": unsure}
def main():
args = parser(__doc__).parse_args()
commands = json.loads((HERE / "function_calling_data.json").read_text())
with client(args) as predict:
emit([run(predict, command) for command in commands], args)
if __name__ == "__main__":
main()
function_calling_data.json
[
"ping me about this tomorrow morning",
"send this to the design team but they shouldn't be able to change anything",
"my eyes hurt at night, can you make the screen darker",
"I need this as a word file for my manager",
"can you remind me next week? actually make it a pdf instead",
"what's the weather in Shanghai",
"make it look like whatever my phone is set to"
]
输出
五条指令变成了函数调用。"Remind me next week? actually make it a pdf" 包含两个请求,所以应用会问用户要哪一个。"What's the weather in Shanghai" 落在中间区间,得到帮助文本。
录制的输出
[
{
"command": "ping me about this tomorrow morning",
"is_command": 0.8666370123638059,
"confidence": 0.8722163963755449,
"status": "call",
"call": {
"function": "set_reminder",
"kwargs": {
"when": "tomorrow"
}
},
"unsure": []
},
{
"command": "send this to the design team but they shouldn't be able to change anything",
"is_command": 0.8469533829065193,
"confidence": 0.9912622488466675,
"status": "call",
"call": {
"function": "share_note",
"kwargs": {
"permission": "view"
}
},
"unsure": []
},
{
"command": "my eyes hurt at night, can you make the screen darker",
"is_command": 0.8734981417655945,
"confidence": 0.8097805124404491,
"status": "call",
"call": {
"function": "change_theme",
"kwargs": {
"theme": "dark"
}
},
"unsure": []
},
{
"command": "I need this as a word file for my manager",
"is_command": 0.9875167965186418,
"confidence": 0.944616775424281,
"status": "call",
"call": {
"function": "export_note",
"kwargs": {
"format": "docx"
}
},
"unsure": []
},
{
"command": "can you remind me next week? actually make it a pdf instead",
"is_command": 0.9193248221227885,
"confidence": 0.5059124954600235,
"status": "clarify",
"options": [
"set_reminder",
"export_note"
]
},
{
"command": "what's the weather in Shanghai",
"is_command": 0.3065500855445862,
"confidence": 0.4502609170856662,
"status": "show_help"
},
{
"command": "make it look like whatever my phone is set to",
"is_command": 0.9843640593466292,
"confidence": 0.9998406975518448,
"status": "call",
"call": {
"function": "change_theme",
"kwargs": {
"theme": "system"
}
},
"unsure": []
}
]
改成你自己的
- 在
TOOLS里加一项就能新增函数,问题会自动生成。自由文本、数字和日期留给代码处理(见选出正确的日期)。 - 按用户想做的事来描述每个函数("Send or share this note with other people"),而不是用它的 API 名称。
is_command的措辞直接点名了容易混淆的情况:需要外部信息(如天气或新闻)的请求。