当前位置:   article > 正文

关于chatglm3 function calling的理解

chatglm3 function calling

ChatGLM3-6B开源了工具调用,好奇他是怎么实现的,所以写了这个文章记录。

一、测试官方的示例

官方给的示例很简单,只不过给的两个函数 track 和 text-to-speech 没有具体的实现,模型的输出也只是给出了需要调用的函数名和参数。剩下的需要自己去实现..

我更换了tools中的函数:

  1. tools = [
  2. {
  3. "name": "go_ahead",
  4. "description": "小车前进",
  5. "parameters": {
  6. "type": "object",
  7. "properties": {
  8. "distance": {
  9. "description": "前进的距离,单位为米"
  10. }
  11. },
  12. "required": ['distance']
  13. }
  14. },
  15. {
  16. "name": "back",
  17. "description": "小车后退",
  18. "parameters": {
  19. "type": "object",
  20. "properties": {
  21. "distance": {
  22. "description": "后退的距离,单位为米"
  23. }
  24. },
  25. "required": ['distance']
  26. }
  27. },
  28. {
  29. "name": "turn_left",
  30. "description": "小车左转",
  31. "parameters": {
  32. "type": "object",
  33. "properties": {
  34. "angle": {
  35. "description": "左转角度,单位为°"
  36. }
  37. },
  38. "required": ['angle']
  39. }
  40. },
  41. {
  42. "name": "turn_right",
  43. "description": "小车右转",
  44. "parameters": {
  45. "type": "object",
  46. "properties": {
  47. "angle": {
  48. "description": "右转角度,单位为°"
  49. }
  50. },
  51. "required": ['angle']
  52. }
  53. }
  54. ]

测试下来出现以下问题:

        1. 输入多个操作只能执行一个操作

        2. 会出现输出不存在的函数的情况

        3. 当已有的函数不能实现用户的操作时,会调用已有函数强行输出

二、代码解析

现在让我们来看看具体实现的代码。下载chatglm3-6b权重的时候也会下载modeling_chatglm.py和tokenization_chatglm.py这两个python文件,chatglm3实现function calling也是在这里面实现的。

1. 输入的处理

首先工具调用跟一般的对话的输入差在有一个 system_info ,他是作为history输入到model.chat函数中的。

system_info = {"role": "system", "content": "Answer the following questions as best as you can. You have access to the following tools:", "tools": tools}

我们可以在modeling_chatglm.py文件中找到chat的实现

  1. @torch.inference_mode()
  2. def chat(self, tokenizer, query: str, history: List[Tuple[str, str]] = None, role: str = "user",
  3. max_length: int = 8192, num_beams=1, do_sample=True, top_p=0.8, temperature=0.8, logits_processor=None,
  4. **kwargs):
  5. if history is None:
  6. history = []
  7. if logits_processor is None:
  8. logits_processor = LogitsProcessorList()
  9. logits_processor.append(InvalidScoreLogitsProcessor())
  10. gen_kwargs = {"max_length": max_length, "num_beams": num_beams, "do_sample": do_sample, "top_p": top_p,
  11. "temperature": temperature, "logits_processor": logits_processor, **kwargs}
  12. inputs = tokenizer.build_chat_input(query, history=history, role=role)
  13. inputs = inputs.to(self.device)
  14. eos_token_id = [tokenizer.eos_token_id, tokenizer.get_command("<|user|>"),
  15. tokenizer.get_command("<|observation|>")]
  16. outputs = self.generate(**inputs, **gen_kwargs, eos_token_id=eos_token_id)
  17. outputs = outputs.tolist()[0][len(inputs["input_ids"][0]):-1]
  18. response = tokenizer.decode(outputs)
  19. history.append({"role": role, "content": query})
  20. response, history = self.process_response(response, history)
  21. return response, history

在chat函数中,history又被作为参数送到tokenizer.build_chat_input中,然后得到input。

那很明显需要查看tokenizer.build_chat_input的实现,tokenizer.build_chat_input函数在tokenization_chatglm中:

  1. def build_chat_input(self, query, history=None, role="user"):
  2. if history is None:
  3. history = []
  4. input_ids = []
  5. for item in history:
  6. content = item["content"]
  7. if item["role"] == "system" and "tools" in item:
  8. content = content + "\n" + json.dumps(item["tools"], indent=4, ensure_ascii=False)
  9. input_ids.extend(self.build_single_message(item["role"], item.get("metadata", ""), content))
  10. input_ids.extend(self.build_single_message(role, "", query))
  11. input_ids.extend([self.get_command("<|assistant|>")])
  12. return self.batch_encode_plus([input_ids], return_tensors="pt", is_split_into_words=True)

根据上面的代码看得出来,他是直接用json.dumps把tools拼接到content中,然后塞给大模型的。

2. 输出的处理

输出的处理在chat函数中的process_response函数

  1. def process_response(self, output, history):
  2. content = ""
  3. history = deepcopy(history)
  4. for response in output.split("<|assistant|>"):
  5. metadata, content = response.split("\n", maxsplit=1)
  6. if not metadata.strip():
  7. content = content.strip()
  8. history.append({"role": "assistant", "metadata": metadata, "content": content})
  9. content = content.replace("[[训练时间]]", "2023年")
  10. else:
  11. history.append({"role": "assistant", "metadata": metadata, "content": content})
  12. if history[0]["role"] == "system" and "tools" in history[0]:
  13. content = "\n".join(content.split("\n")[1:-1])
  14. def tool_call(**kwargs):
  15. return kwargs
  16. parameters = eval(content)
  17. content = {"name": metadata.strip(), "parameters": parameters}
  18. else:
  19. content = {"name": metadata.strip(), "content": content}
  20. return content, history

这里需要注意一点,chatglm3-6b应该是有针对工具调用进行训练,输出的结果很稳定,基本上都是下面的结构:

'turn_right\n```python\ntool_call(angle=30)\n```'

第一行是调用的函数名,然后下面是执行函数的代码(代码中函数名统一为tool_call)。再通过split('\n')得到代码,eval执行tool_call函数得到函数的变量字典,然后返回字典如下:

{'name': 'turn_right', 'parameters': {'angle': 30}}

3.  openai_api_demo.py

官方还给出了openai_api_demo.py这个文件,他实现了完整的 输入自然语言->得到函数和函数参数->执行函数 这一套流程。虽然不知道为什么没有在readme中写出来

openai_api_demo.py主要依靠tool_register.py下的get_tools和dispatch_tool

  1. 1. register_tool用于注册函数,它接受一个可调用对象 func 作为参数。该函数将 func 注册为一个工具,并返回 func 本身。
  2. 2. dispatch_tool用于执行函数,它接受一个函数名和函数参数,返回函数的返回。

三、尝试在百川上实现

我是在baichaun-13B上进行测试的,相对于chatglm3-6b每次稳定的输出,baichaun-13B的输出就不是那么好了。所以我们需要设计一个prompt如下:

  1. prompt = '''
  2. 输出必须是带有markdown格式的python代码:
  3. ```python
  4. 工具的name(parameters)
  5. ```
  6. 例如:
  7. ```python
  8. back(distance=10)
  9. ```'''

那么输入到百川模型的messages如下:

  1. system_info = {"role": "system", "content": "尽可能回答以下问题。您可以使用以下工具:\n tools:" + json.dumps(tools, indent=4, ensure_ascii=False) + prompt}
  2. query = "23加69等于多少"
  3. messages = [
  4. system_info,
  5. {"role": "user", "content": query}
  6. ]

没有意外的话模型会生成一个被```python```包裹的代码,使用eval()执行代码就可以了

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小桥流水78/article/detail/856660
推荐阅读
相关标签
  

闽ICP备14008679号