当前位置:   article > 正文

最好的Prompt管理和使用依然是 Class 和 Function - 继续让LLM和编程语言融合

byzer-retrieval

问题

Python 语言其实已经是对字符串模板最友好的语言之一了,但是实际写出来是这样的:

dba4841ba31d8dcdc8de8c576533e23d.png

实际prompt 一般都会远大于上面的例子。而且我们可以看到缩进也完全break掉了,这在Python中会导致源码很难看。如果你在方法里定义了prompt, 那么缩进就更是灾难了。

上面的基本思路是在定义一个或者多个python文件,里面专门写很多上面例子的大段大段的话,然后使用的时候还需要配合使用专门封装函数(比如PromptTemplate对象)做渲染。比如流行的langchain/llama_index的做法就是这样:

28aad11a3ddb0fd0e878680059b97ea7.png

为了能够解决缩进问题,他们使用了 Tuple的方式,虽然美观了,但是写和修改都会痛苦万分。另外用起来其实也很不方便,还是老套路。

而且,Python已经是对“文本”特别友好的语言了,其他语言你估计会吐血。

归根结底,就是“prompt 编程” 和“传统编程” 是不match的,导致各种别扭和难受。

业绩已有的探索

为了解决上面的问题,业界也做了很多探索。比如DSPy 就提供了很多常见模板,然后以class的方式填写:

  1. class GenerateAnswer(dspy.Signature):
  2. """Answer questions with short factoid answers."""
  3. context = dspy.InputField(desc="may contain relevant facts")
  4. question = dspy.InputField()
  5. answer = dspy.OutputField(desc="often between 1 and 5 words")

很像 Pydantic, 然后他会根据你的这个类,和内置的模板做结合:

  1. # Option 1: Pass minimal signature to ChainOfThought module
  2. generate_answer = dspy.ChainOfThought("context, question -> answer")
  3. # Option 2: Or pass full notation signature to ChainOfThought module
  4. generate_answer = dspy.ChainOfThought(GenerateAnswer)
  5. # Call the module on a particular input.
  6. pred = generate_answer(context = "Which meant learning Lisp, since in those days Lisp was regarded as the language of AI.",
  7. question = "What programming language did the author learn in college?")

实际上配置了一个 ChainOfThout的模板,自动从类提取信息,然后做渲染。实际上这个方案没有从根本上解决prompt的管理,如果没有模板呢?而且用户需要学习大量API,我觉得大概率用的人不会多,心智门槛太高了。

另外还有一个心智门槛更高的SGlang 里面用到的语法:

  1. from sglang import function, system, user, assistant, gen, set_default_backend, RuntimeEndpoint
  2. @function
  3. def multi_turn_question(s, question_1, question_2):
  4. s += system("You are a helpful assistant.")
  5. s += user(question_1)
  6. s += assistant(gen("answer_1", max_tokens=256))
  7. s += user(question_2)
  8. s += assistant(gen("answer_2", max_tokens=256))
  9. set_default_backend(RuntimeEndpoint("http://localhost:30000"))
  10. state = multi_turn_question.run(
  11. question_1="What is the capital of the United States?",
  12. question_2="List two local attractions.",
  13. )

这种不但把 Prompt搞复杂了,还把 Python代码搞复杂了,说实在的,我不会多看一眼。。。

Byzer-LLM 解决方案

我经过很长的一段时间实践,我发现要回归到本源,也就是你终究是在写代码, prompt只是代码里的一部分,那么要解决prompt的管理,还是要从 Class/Function 这种方式去解决。Class/Function就是编程语言的一个抽象范式,帮你管理和使用各种功能。同理,一段Prompt就应该是一个Function, 多个Prompt应该就可以组成一个类,这些prompt要组成一个类,就意味着他们有内在的关系。

此外,我们还要解决文本在 Python 中缩进的问题,避免我们提到的LlamaIndex等库里的问题。

经过这些思考,最终我们得到了一个新的设计。

  1. Prompt 函数

  2. Prompt 类

后续文中的效果大家都可以在 Byzer-LLM 0.1.44版本体验到。

首先,我们部署一个 SaaS模型:

  1. import os
  2. os.environ["RAY_DEDUP_LOGS"] = "0"
  3. import ray
  4. from byzerllm.utils.retrieval import ByzerRetrieval
  5. from byzerllm.utils.client import ByzerLLM,LLMRequest,LLMResponse,LLMHistoryItem,InferBackend
  6. from byzerllm.utils.client import Templates
  7. ray.init(address="auto",namespace="default",ignore_reinit_error=True)
  8. llm = ByzerLLM()
  9. llm.setup_num_workers(2).setup_gpus_per_worker(0)
  10. llm.deploy(pretrained_model_type="saas/sparkdesk",
  11. udf_name="sparkdesk_chat",
  12. infer_params={
  13. "saas.appid":"xxxx",
  14. "saas.api_key":"xxxx",
  15. "saas.api_secret":"xxxx",
  16. "saas.gpt_url":"wss://spark-api.xf-yun.com/v3.5/chat"
  17. })

这里用了讯飞的星火大模型,token管饱。

Prompt 函数

接着,我们模拟一个非常典型的RAG Prompt,我们使用函数来做 Prompt的载体:

  1. @llm.prompt()
  2. def generate_answer(context:str,question:str)->str:
  3. '''
  4. Answer the question based on only the following context:
  5. {context}
  6. Question: {question}
  7. Answer:
  8. '''
  9. pass

我们定义了一个叫做 generate_answer, 他的doc其实就是我们要的prompt, 这里,这个Prompt包含了一些变量: {context},{question}。这个变量可以通过generate_answer的方法进行传递。

方法本身不需要做任何实现,唯一和别人不同的地方是,他有个 @llm.prompt() 注解。That's all。

  1. context='''
  2. Byzer产品栈从底层存储到大模型管理和serving再到应用开发框架:
  3. 1. Byzer-Retrieval, 一个支持向量+搜索的混合检索数据库。
  4. 2. Byzer-LLM, 可以衔接SaaS/开源模型,能部署,可以统一入口。
  5. 3. Byzer-Agent ,一套分布式 Agent 框架,做为应用开发框架
  6. 4. ByzerPerf, 一套性能吞吐评测框架
  7. 5. ByzerEvaluation, 一套大模型效果评测框架 (未开源)
  8. 6. Byzer-SQL, 一套全SQL方言,支持ETL,数据分析等工作,并且支持大模型的管理和调用(model as UDF),方便数据预处理。
  9. '''
  10. print(generate_answer(context=context,question="Byzer SQL是什么?"))

假设我们从数据库里召回了一段上下文,然后从HTTP接口拿到了一个问题,我们直接调用 generate_answer 方法即可完成,输出如下:

Byzer SQL 是一个全SQL方言,支持ETL、数据分析等工作,并且支持大模型的管理和调用(model as UDF),方便数据预处理。

从上面的例子可以看到,prompt 被转变成一个函数,而这个函数的实现,实际上就是 doc ,而不是传统意义上的你的实现代码(这里是pass)。现在,我们真正意义上实现了,doc就是实现。

Doc 里的变量可以和函数入参自动结合渲染,并且最终被大模型运行。

并且由于 Doc 提供了良好的缩进和多行控制能力,所以整个观感也非常好。你写个一百个函数,也不会觉得格式乱。

为了解决一个问题,你可能需要和多个prompt,那么这些prompt 函数就可以放在一个类。下面我们来看看怎么解决。

Prompt 类

  1. import ray
  2. from byzerllm.utils.client import ByzerLLM
  3. import byzerllm
  4. ray.init(address="auto",namespace="default",ignore_reinit_error=True)
  5. class RAG():
  6. def __init__(self):
  7. self.llm = ByzerLLM()
  8. self.llm.setup_template(model="sparkdesk_chat",template="auto")
  9. self.llm.setup_default_model_name("sparkdesk_chat")
  10. @byzerllm.prompt(lambda self: self.llm)
  11. def generate_answer(self,context:str,question:str)->str:
  12. '''
  13. Answer the question based on only the following context:
  14. {context}
  15. Question: {question}
  16. Answer:
  17. '''
  18. pass

这里,我定义了一个叫 RAG 的类,然后初始化的时候初始化了一个大模型client。然后里面定义了一个叫 generate_answer 的方法,这个方法和前面的方法是完全一样的。唯一的区别他现在是一个实例方法。

注解也有一点点变化,使用 byzerllm 模块的prompt 装饰器。其中第一个参数是一个lambda表达式,这个表达式会传递 RAG 实例的 llm 引用。

定义完上面的代码后,我们现在就可以直接使用了:

  1. t = RAG()
  2. print(t.generate_answer(context=context,question="Byzer SQL是什么?"))

如果你希望不要执行这个prompt,而是拿到渲染后的prompt,那么可以这么做:

  1. import ray
  2. from byzerllm.utils.client import ByzerLLM
  3. import byzerllm
  4. ray.init(address="auto",namespace="default",ignore_reinit_error=True)
  5. class RAG():
  6. def __init__(self):
  7. self.llm = ByzerLLM()
  8. self.llm.setup_template(model="sparkdesk_chat",template="auto")
  9. self.llm.setup_default_model_name("sparkdesk_chat")
  10. @byzerllm.prompt()
  11. def generate_answer(self,context:str,question:str)->str:
  12. '''
  13. Answer the question based on only the following context:
  14. {context}
  15. Question: {question}
  16. Answer:
  17. '''
  18. pass
  19. t = RAG()
  20. print(t.generate_answer(context=context,question="Byzer SQL是什么?"))

和第一版代码的唯一区别就是没有传递 llm 引用。这里会直接输出渲染后的prompt:

  1. Answer the question based on only the following context:
  2. Byzer产品栈从底层存储到大模型管理和serving再到应用开发框架:
  3. 1. Byzer-Retrieval, 一个支持向量+搜索的混合检索数据库。
  4. 2. Byzer-LLM, 可以衔接SaaS/开源模型,能部署,可以统一入口。
  5. 3. Byzer-Agent ,一套分布式 Agent 框架,做为应用开发框架
  6. 4. ByzerPerf, 一套性能吞吐评测框架
  7. 5. ByzerEvaluation, 一套大模型效果评测框架 (未开源)
  8. 6. Byzer-SQL, 一套全SQL方言,支持ETL,数据分析等工作,并且支持大模型的管理和调用(model as UDF),方便数据预处理。
  9. Question: Byzer SQL是什么?
  10. Answer:

Prompt 函数返回值

前面的例子,我们的Prompt函数都是直接返回字符串。如果我希望他返回结果化数据呢?当然没问题,Prompt 函数可以支持两种类型:Pydantic 和 Str. 我们来举个返回值是 Pydantic Model 的例子:

  1. import ray
  2. import functools
  3. import inspect
  4. import byzerllm
  5. import pydantic
  6. ray.init(address="auto",namespace="default",ignore_reinit_error=True)
  7. class ByzerProductDesc(pydantic.BaseModel):
  8. byzer_retrieval: str
  9. byzer_llm: str
  10. byzer_agent: str
  11. byzer_perf: str
  12. byzer_evaluation: str
  13. byzer_sql: str
  14. class RAG():
  15. def __init__(self):
  16. self.llm = ByzerLLM()
  17. self.llm.setup_template(model="sparkdesk_chat",template="auto")
  18. self.llm.setup_default_model_name("sparkdesk_chat")
  19. @byzerllm.prompt(lambda self: self.llm)
  20. def generate_answer(self,context:str,question:str)->ByzerProductDesc:
  21. '''
  22. Answer the question based on only the following context:
  23. {context}
  24. Question: {question}
  25. Answer:
  26. '''
  27. pass
  28. t = RAG()
  29. byzer_product = t.generate_answer(context=context,question="Byzer 产品列表")
  30. print(byzer_product.byzer_sql)
  31. ## output: 一套全SQL方言,支持ETL,数据分析等工作,并且支持大模型的管理和调用(model as UDF),方便数据预处理

在这个例子中,我们定义了一个叫做 ByzerProductDesc 的类,并且作为 generate_answer 的返回值。

现在,当我们运行 prompt 函数 generate_answer 的时候,返回的就是 ByzerProductDesc对象而不是字符串。

Prompt函数可编程性

Prompt 函数本质是 Doc 部分取代了传统编码,但是 Doc 本省就是“程序”,我们称为 Prompt Programming. 前面我们看到, Prompt 函数里的 Doc 仅仅能填写一些变量,这些变量会自动被函数入参替换。如果我希望 Doc 也是真正的可编程,包括对参数做处理,怎么办?当然没问题,Byzer-LLM 引入了 Jinjia 模板技术。让我们看看:

  1. import ray
  2. import functools
  3. import inspect
  4. import byzerllm
  5. import pydantic
  6. from byzerllm.utils.client import ByzerLLM
  7. ray.init(address="auto",namespace="default",ignore_reinit_error=True)
  8. data = {
  9. 'name': 'Jane Doe',
  10. 'task_count': 3,
  11. 'tasks': [
  12. {'name': 'Submit report', 'due_date': '2024-03-10'},
  13. {'name': 'Finish project', 'due_date': '2024-03-15'},
  14. {'name': 'Reply to emails', 'due_date': '2024-03-08'}
  15. ]
  16. }
  17. class RAG():
  18. def __init__(self):
  19. self.llm = ByzerLLM()
  20. self.llm.setup_template(model="sparkdesk_chat",template="auto")
  21. self.llm.setup_default_model_name("sparkdesk_chat")
  22. @byzerllm.prompt(render="jinja2")
  23. def generate_answer(self,name,task_count,tasks)->str:
  24. '''
  25. Hello {{ name }},
  26. This is a reminder that you have {{ task_count }} pending tasks:
  27. {% for task in tasks %}
  28. - Task: {{ task.name }} | Due: {{ task.due_date }}
  29. {% endfor %}
  30. Best regards,
  31. Your Reminder System
  32. '''
  33. pass
  34. t = RAG()
  35. response = t.generate_answer(**data)
  36. print(response)

和前面的代码,有三个地方发生了变化:

  1.  @byzerllm.prompt(render="jinja2") 里多了一个参数 render, 该值被设计为 jinja2了。

  2. generate_answer 里的 Doc 实现,采用了 jinjia2 语法。

  3. 参数我改成了 name,task_count,tasks。其中 tasks 是一个比较复杂的结构类型。

generate_answer 在 doc 中实现了对 tasks 参数做了做循环和使用,从而实现更好的模板控制。

最后来个回顾

我们提出了 Prompt函数, Prompt 类的概念,将Prompt 和 函数实现了完美结合,Prompt函数会自动将入参渲染到 文本中,与此同时,Prompt函数还支持字符串和复杂结构返回。

Prompt函数的核心是利用文本替换了代码实现,通过引入jinjia强大的模板能力,可以实现复杂的参数渲染,并且可以通过配置开关选择返回渲染后的Prompt或者大模型执行后的Prompt。

最后的最后

Byzer-LLM 在大语言模型和编程融合上做了非常多的探索,大家还可以看看我们往期的文章:

函数实现越通用越好?来看看 Byzer-LLM 的 Function Implementation 带来的编程思想大变化

Python 和 LLM 的完美融合之路 (再谈 Function Impl)

给开源大模型带来Function Calling、 Respond With Class

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

闽ICP备14008679号