kimi联网搜索tool的流式输出教程

最近,moonshot开放了kimi的联网搜索接口,非常滴好用(直接干掉了博主写的多agent联网搜索框架)。但是,官方所提供的使用代码并不支持流式输出,这导致该接口无法与博主的现有工程兼容,为此,博主自研了一套流式输出方案。

先看kimi开发文档中的代码(不支持流式输出):

from typing import *
 
import os
import json
 
from openai import OpenAI
from openai.types.chat.chat_completion import Choice
 
client = OpenAI(
    base_url="https://api.moonshot.cn/v1",
    api_key=os.environ.get("MOONSHOT_API_KEY"),
)
 
 
# search 工具的具体实现,这里我们只需要返回参数即可
def search_impl(arguments: Dict[str, Any]) -> Any:
    """
    在使用 Moonshot AI 提供的 search 工具的场合,只需要原封不动返回 arguments 即可,
    不需要额外的处理逻辑。
 
    但如果你想使用其他模型,并保留联网搜索的功能,那你只需要修改这里的实现(例如调用搜索
    和获取网页内容等),函数签名不变,依然是 work 的。
 
    这最大程度保证了兼容性,允许你在不同的模型间切换,并且不需要对代码有破坏性的修改。
    """
    return arguments
 
 
def chat(messages) -> Choice:
    completion = client.chat.completions.create(
        model="moonshot-v1-128k",
        messages=messages,
        temperature=0.3,
        tools=[
            {
                "type": "builtin_function",  # <-- 使用 builtin_function 声明 $web_search 函数,请在每次请求都完整地带上 tools 声明
                "function": {
                    "name": "$web_search",
                },
            }
        ]
    )
    return completion.choices[0]
 
 
def main():
    messages = [
        {"role": "system", "content": "你是 Kimi。"},
    ]
 
    # 初始提问
    messages.append({
        "role": "user",
        "content": "请搜索 Moonshot AI Context Caching 技术,并告诉我它是什么。"
    })
 
    finish_reason = None
    while finish_reason is None or finish_reason == "tool_calls":
        choice = chat(messages)
        finish_reason = choice.finish_reason
        if finish_reason == "tool_calls":  # <-- 判断当前返回内容是否包含 tool_calls
            messages.append(choice.message)  # <-- 我们将 Kimi 大模型返回给我们的 assistant 消息也添加到上下文中,以便于下次请求时 Kimi 大模型能理解我们的诉求
            for tool_call in choice.message.tool_calls:  # <-- tool_calls 可能是多个,因此我们使用循环逐个执行
                tool_call_name = tool_call.function.name
                tool_call_arguments = json.loads(tool_call.function.arguments)  # <-- arguments 是序列化后的 JSON Object,我们需要使用 json.loads 反序列化一下
                if tool_call_name == "$web_search":
                    tool_result = search_impl(tool_call_arguments)
                else:
                    tool_result = f"Error: unable to find tool by name '{tool_call_name}'"
 
                # 使用函数执行结果构造一个 role=tool 的 message,以此来向模型展示工具调用的结果;
                # 注意,我们需要在 message 中提供 tool_call_id 和 name 字段,以便 Kimi 大模型
                # 能正确匹配到对应的 tool_call。
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "name": tool_call_name,
                    "content": json.dumps(tool_result),  # <-- 我们约定使用字符串格式向 Kimi 大模型提交工具调用结果,因此在这里使用 json.dumps 将执行结果序列化成字符串
                })
 
    print(choice.message.content)  # <-- 在这里,我们才将模型生成的回复返回给用户
 
 
if __name__ == '__main__':
    main()
 

其流程为:

1.当 Kimi 大模型生成了 finish_reason=tool_calls 的响应时,表明 Kimi 大模型已经意识到当前需要执行 $web_search 函数,并且也已经做好执行 $web_search 的一切准备工作;

2.Kimi 大模型会将执行函数所必须得参数以 tool_call.function.arguments 的形式返回给调用方,但这些参数并不由调用方执行,调用方只需要将 tool_call.function.arguments 原封不动地提交给 Kimi 大模型,即可由 Kimi 大模型执行对应的联网搜索流程;

3.当用户将 tool_call.function.arguments 使用 role=tool 的 message 提交时,Kimi 大模型随即开始执行联网搜索流程,并根据搜索和阅读结果生成可供用户阅读的消息,即 finish_reason=stop 的 message。

经过博主试验发现,上述代码的主while循环只会执行两次:

第一次循环中,finish_reason = choice.finish_reason 产生了 finish_reason=tool_calls 的响应。使得 if finish_reason=tool_calls 的判断为True,执行了联网tool的调用,并将联网搜索的结果记录到message中。

第二次循环中,在choice = chat(messages) 一步就产生了最终回答,通过print(choice.message.content) 将模型生成的回复打印到工作台。

可以看到,联网搜索的结果都是在第一次循环时产生的,而第二次循环只是让kimi生成给用户的最终回答。因此,我们只需要在第二次循环中设定kimi的回答方式是流式的即可。

基于这个思路,博主将代码进行了改良:

请将文件命名为:kimi.py 

from typing import *
import os
import json
from openai import OpenAI
from openai.types.chat.chat_completion import Choice

# 初始化 Moonshot AI 客户端,设置 API 的基础 URL 和 API 密钥。
client = OpenAI(
    base_url="https://api.moonshot.cn/v1",
    api_key="填写你申请的api-key",
)


# search_impl 函数是一个占位符,用于模拟联网搜索工具的调用。
# 在实际使用中,这里可以调用外部搜索服务,并返回搜索结果。
def search_impl(arguments: Dict[str, Any]) -> Any:
    """
    在使用 Moonshot AI 提供的 search 工具的场合,只需要原封不动返回 arguments 即可,
    不需要额外的处理逻辑。
    但如果你想使用其他模型,并保留联网搜索的功能,那你只需要修改这里的实现(例如调用搜索
    和获取网页内容等),函数签名不变,依然是 work 的。
    """
    return arguments


# chat 函数是主要的聊天处理函数,它发送消息给 Moonshot AI 的 API 并接收回复。
def chat(messages) -> Choice:  # 返回的是Choice类型
    # 使用 Moonshot AI 的 chat.completions.create 方法发送消息并获取回复。
    completion = client.chat.completions.create(
        model="moonshot-v1-128k",  # 使用的模型名称
        messages=messages,  # 发送的消息列表
        temperature=0.3,  # 控制回复的随机性,值越高回复越多样化
        tools=[  # 定义可以使用的工具,这里使用了内置的 web_search 工具
            {
                "type": "builtin_function",
                "function": {
                    "name": "$web_search",
                },
            }
        ]
    )
    usage = completion.usage  # 获取 API 使用情况
    choice = completion.choices[0]  # 获取第一条回复

    # 打印出聊天过程中消耗的 Tokens 数量
    if choice.finish_reason == "stop":
        print(f"chat_total_tokens:           {usage.total_tokens}")

    return choice


def stream(messages: list) -> Choice:
    completion = client.chat.completions.create(
        model="moonshot-v1-128k",
        messages=messages,
        temperature=0.3,
        tools=[  # 定义可以使用的工具
            {
                "type": "builtin_function",
                "function": {
                    "name": "$web_search",
                },
            }
        ],
        stream=True  # 开启流式处理
    )
    return completion


# main 函数是程序的入口点,它处理用户的提问并调用 chat 函数。
def main(question, chat_history: list = []):
    Question = question + "如果联网搜索结果有参考意义,则你可在回答中引用链接"

    messages = [
        {"role": "system", "content": "你是 Kimi。"},  # 系统角色的消息,定义了聊天的上下文
    ]

    # 初始提问
    messages.append({
        "role": "user",
        "content": Question
    })
    flag = 0
    finish_reason = None

    chat_history.append((Question, "联网检索中..."))

    while finish_reason is None or finish_reason == "tool_calls":  # 如果 finish_reason 是 "stop",则表示聊天已经结束,循环将终止。
        #
        print("循环")
        if flag == 1:
            print("流式")
            choice = stream(messages)
            ans = ""
            for chunk in choice:

                delta = chunk.choices[0].delta  # <-- message 字段被替换成了 delta 字段

                if delta.content:
                    # 我们在打印内容时,由于是流式输出,为了保证句子的连贯性,我们不人为地添加
                    # 换行符,因此通过设置 end="" 来取消 print 自带的换行符。
                    print(delta.content, end="")
                    ans = ans + delta.content
                chat_history[-1] = (question, ans)
                yield "", chat_history
            break

        choice = chat(messages)  # 调用 chat 函数处理消息
        #
        finish_reason = choice.finish_reason  # 获取聊天完成的原因

        if finish_reason == "tool_calls":  # 说明此次循环kimi意图识别为调用搜索工具
            flag = 1
            messages.append(choice.message)  # 将回复添加到消息列表中
            for tool_call in choice.message.tool_calls:
                tool_call_name = tool_call.function.name  # 获取工具调用的名称
                print(tool_call.function)
                tool_call_arguments = json.loads(  # 解析工具调用的参数
                    tool_call.function.arguments)
                print(tool_call_arguments)
                if tool_call_name == "$web_search":
                    # 如果工具调用是 web_search,则调用 search_impl 函数
                    search_content_total_tokens = tool_call_arguments.get("usage", {}).get("total_tokens")
                    print(f"search_content_total_tokens: {search_content_total_tokens}")
                    tool_result = search_impl(tool_call_arguments)
                else:
                    tool_result = f"Error: unable to find tool by name '{tool_call_name}'"

                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "name": tool_call_name,
                    "content": json.dumps(tool_result),  # 将工具调用的结果添加到消息列表中
                })

    

可以看到,在第二次循环时,执行的是如下的代码:

     if flag == 1:
            print("流式")
            choice = stream(messages)
            ans = ""
            for chunk in choice:

                delta = chunk.choices[0].delta  # <-- message 字段被替换成了 delta 字段

                if delta.content:
                    # 我们在打印内容时,由于是流式输出,为了保证句子的连贯性,我们不人为地添加
                    # 换行符,因此通过设置 end="" 来取消 print 自带的换行符。
                    print(delta.content, end="")
                    ans = ans + delta.content
                chat_history[-1] = (question, ans)
                yield "", chat_history
            break

为了实现流式输出,main函数的返回方式改为for+yield的组合形式,而非return。

同时,为了方便演示,博主将输出由工作台动态打印的同时,也加入了基于Gradio库的界面展示,代码如下:

详细原理请参考博主的Gradio相关教程。

import gradio as gr
# 检错
from kimi import main
import time

# init
print("init")
start_time = time.time()

# 加主题风格theme
google_font_primary = gr.themes.GoogleFont("Roboto")  # 例如使用 "Roboto" 作为主要字体
google_font_mono = gr.themes.GoogleFont("Roboto Mono")  # 使用 "Roboto Mono" 作为等宽字体
theme = gr.themes.Glass(

    font=[google_font_primary],
    font_mono=[google_font_mono],
    primary_hue="gray",
    secondary_hue="blue"

).set(

    background_fill_secondary="*primary_300",
    input_background_fill="linear-gradient(180deg, *primary_100 0%, white 100%)",  # 输入框

)

# 创建一个 Web 界面
with gr.Blocks(theme=theme) as demo:
    demo.title = "kimi"
    demo.description = "kimi for research"
    demo.css = """
            .title {
                text-align: center; /* 文字居中 */
                background:  #0F0000 ; /* 背景色 */
                border-radius: 10px; /* 圆角边框 */
                padding: 18px; /* 内边距 */     
                font-size: 2em; /* 字体大小 */
                text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); /* 文字阴影 */
                margin: 10px 0; /* 外边距 */
                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); /* 边框阴影效果 */

            }

        """

    with gr.Row(equal_height=True):
        with gr.Column(scale=15):
            gr.Markdown("""<h1 class="title" style="color: #FFFFFF;" >kimi for research</h1>""")

    with gr.Row():
        with gr.Column(scale=4):
            # 创建一个聊天机器人对象
            chatbot = gr.Chatbot(height=450, show_copy_button=True)
            # 创建一个文本框组件,用于输入 prompt。
            query = gr.Textbox(label="Prompt/问题")
            with gr.Row():
                # 创建提交按钮。
                db_wo_his_btn = gr.Button("对话")
            with gr.Row():
                # 创建一个清除按钮,用于清除聊天机器人组件的内容。
                clear = gr.ClearButton(
                    components=[chatbot], value="清空记录")

        # 设置按钮的点击事件。当点击时,调用上面定义的 qa_chain_self_answer 函数,并传入用户的消息和聊天历史记录,然后更新文本框和聊天机器人组件。
        # 上传
        db_wo_his_btn.click(main, inputs=[query, chatbot], outputs=[query, chatbot])

    gr.Markdown("""提醒:<br>
    1. 联网搜索的时间可能比表长,请耐心等待

      """)
    gr.close_all()
    demo.queue()
    demo.launch()

运行之,效果如下图所示:

输入的搜索内容:广东省数字孪生人重点实验室

1.结果动态打印在工作台

2.结果流式输出在UI

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

仙人球小熊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值