本文面向已了解 Pro Config 基础的 MyShell 创作者。如需了解 Pro Config,请参考 「MyShell 进阶 - 入门 Pro Config」
Pro Config 是 MyShell 提供的 Bot 创作新方式,可以用低代码方式快速构建功能强大的 AI 应用。第三期 Pro Config Learning Lab 活动现正热招中,欢迎具备编程基础的创作者报名参与,在一周内完成应用开发者可获得 1 至 2 张价值 1.3K USD 的 MyShell GenesisPass。我是第一期的毕业生,我把原先完全依靠提示词(Prompt) 编写的 Think Tank 升级为 Think Tank ProConfig,通过了考核。
Think Tank 是 AI 智囊团,它允许用户选取多个领域的专家来探讨复杂问题,并给出跨学科的建议。初始版本的 Think Tank ProConfig 引入了自定义参数,实现了预设特定领域专家和内容国际化输出。
起初,我仅按照教程创建了 Think Tank 的 Pro Config 版来通过考核,但未理解 Pro Config 的全部功能。最近新增了推荐话题功能来促进用户与 AI 的互动。在开发新功能中,我对 Pro Config 有了更深刻的认识。以下总结出了一些官方教程未详细说明的实用技巧。
变量#
Pro Config 的变量分为全局变量和局部变量,具体在官方教程的 expressions and variables 章节中有具体阐述。
全局变量用 context 读写。适合存各种类型的数据:
- string:字符串,可用于保存文本类型的输出,或将 JSON 对象通过 JSON.stringify 序列化后存储。
- prompt: 提示词,将 system_prompt 存入 context,将需传递的变量全部置于 user_prompt。
- list:列表,非字符串类型应使用 {{}} 进行包裹。
- number: 数字,同样需要用
{{}}
包裹 - bool:布尔值,同样需要用
{{}}
包裹 - null: 可空值
- url:外链数据,用于多媒体显示,下面的例子中用
context.emo_list[context.index_list.indexOf(emo)]
来显示特定图片
"context": {
"correct_count": "",
"prompt": "You are a think tank, you task is analyze and discuss questions raised...",
"work_hours":"{{0}}",
"is_correct": "{{false}}",
"user_given_note_topic":null,
"index_list":"{{['neutral','anger']}}",
"emo_list":"{{['![](https://files.catbox.moe/ndkcpp.png)','![](https://files.catbox.moe/pv5ap6.png)']}}
}
局部变量可为上述所有类型。可通过 payload 在状态间传递局部变量(如响应不同按钮事件),详见官方 Pro Config 教程中的 function-calling-example。
开发新状态时,可先使用 render 展示必要变量以验证其正确性后再添加 tasks,这样做可以提升开发效率。
JSON 生成#
若要使用大语言模型(LLM)输出内容作为按钮列表值,如 Think Tank ProConfig 的 2 个 Related Topic 按钮,需设计 prompt 以直接生成供上下文调用的 JSON。
有两种方法来达到该目的:
聚合响应(Aggregate responses):直接生成文本和 JSON 混合的输出,再用 JS 代码处理字符串来获取 JSON。
参考以下 prompt(已省略与生成 JSON 不相关内容)
……
<instructions>
……
6. Show 2 related topics and experts appropriate for further discussion with JSON format in code block
</instructions>
<constraints>
……
- MUST display "Related Topics" in code block
</constraints>
<example>
……
**Related Topics**:
```
[{"question": related_topic_1,"experts": [experts array 1]}, {"question": related_topic_2,"experts": [experts array 2]}]
```
</example>
在用 Google Gemini 调试时,发现在非英文环境下生成 JSON 并不稳定:有时候输出的 JSON 不在 ```
标记的代码块里;有时候会输出 ```json
。都无法简单地用reply.split('```')[1].split('```')[0]
提取。
当发现从聚合响应中提取 JSON 并不稳定时,我选择用额外 LLM task 产生 JSON 数据。
多任务(Multiple Tasks):分别在不同任务(task)中生成回复和 JSON。
参考 生成 JSON 的 prompt 如下:
Based on the discussion history, generate a JSON array containing two related questions the user may be interested in, along with a few relevant domain experts for further discussion on each question.
<constraints>
- The output should be ONLY a valid JSON array.
- Do not include any additional content outside the JSON array.
- Related question should NOT same as origin discussion topic.
</constraints>
<example>
```json
[
{
"question": "Sustainable Living",
"experts": [
"Environmental Scientist",
"Urban Planner",
"Renewable Energy Specialist"
]
},
{
"question": "Mindfulness and Stress Management",
"experts": [
"Meditation Instructor",
"Therapist",
"Life Coach"
]
}
]
```
</example>
参考 Pro Config 如下,其中context.prompt_json
就是上面生成 JSON 的 prompt
……
"tasks": [
{
"name": "generate_reply",
"module_type": "AnyWidgetModule",
"module_config": {
"widget_id": "1744218088699596809", // claude3 haiku
"system_prompt": "{{context.prompt}}",
"user_prompt": "User's question is <input>{{question}}</input>. The reponse MUST use {{language}}. The fields/experts MUST include but not limit {{fields}}.",
"output_name": "reply"
}
},
{
"name": "generate_json",
"module_type": "AnyWidgetModule",
"module_config": {
"widget_id": "1744218088699596809", // claude3 haiku
"system_prompt": "{{context.prompt_json}}",
"user_prompt": "discussion history is <input>{{reply}}</input>. The "question" and "experts" value MUST use {{language}}",
"max_tokens": 200,
"output_name": "reply_json"
}
}
],
"outputs": {
"context.last_discussion_str": "{{ reply }}",
"context.more_questions_str": "{{ reply_json.replace('```json','').replace('```','') }}",
},
……
第一个 task 是用 LLM 创建讨论内容。
第二个 task 读取已有的讨论内容即{{reply}}
,用 LLM 生成了 2 个关联话题的 JSON,接着使用replace
移除代码块标记后,将 JSON 字符串写入到变量context.more_questions_str
中。
一个小技巧是设置 "max_tokens": 200
避免生成长度过长的 JSON。
最终,将该字符串设置为按钮描述(description),并记录用户点击索引值(target_index)来实现状态转换。
……
"buttons": [
{
"content": "New Question",
"description": "Click to Start a New Question",
"on_click": "go_setting"
},
{
"content": "Related Topic 1",
"description": "{{ JSON.parse(context.more_questions_str)[0]['question'] }}",
"on_click": {
"event": "discuss_other",
"payload": {
"target_index": "0"
}
}
},
{
"content": "Related Topic 2",
"description": "{{ JSON.parse(context.more_questions_str)[1]['question'] }}",
"on_click": {
"event": "discuss_other",
"payload": {
"target_index": "1"
}
}
}
]
……
AI Logo 设计应用 AIdea 也使用了这一技巧。AIdea 通过一个独立任务生成 JSON,其它信息则通过提取 context 内容进行字符串连接后,最终进行 render。另外,Aldea 在按钮元素内直接展示了产品名称 —— 不同于 Think Tank ProConfig 将其置于按钮描述里,需鼠标悬停方可查看。
如果 JSON 结构很复杂,还可以利用 GPT 的 function calling 来生成。注意只能在 GPT3.5 和 GPT4 的 LLM widget 中使用,示例如下:
……
"tasks": [
{
"name": "generate_reply",
"module_type": "AnyWidgetModule",
"module_config": {
"widget_id": "1744214024104448000", // GPT 3.5
"system_prompt": "You are a translator. If the user input is English, translate it to Chinese. If the user input is Chinese, translate it to English. The output should be a JSON format with keys 'translation' and 'user_input'.",
"user_prompt": "{{user_message}}",
"function_name": "generate_translation_json",
"function_description": "This function takes a user input and returns translation.",
"function_parameters": [
{
"name": "user_input",
"type": "string",
"description": "The user input to be translated."
},
{
"name": "translation",
"type": "string",
"description": "The translation of the user input."
}
],
"output_name": "reply"
}
}
],
"render": {
"text": "{{JSON.stringify(reply)}}"
},
……
输出结果:
更详细用法可参考 官方 ProConfig Tutorial 中的示例。
记忆(memory)#
使用以下代码把最新聊天消息添加到 memory 中,并将更新后的 memory 通过 LLMModule
的 memory
参数传递给 LLM,使其能够根据之前的交互记录作出响应。
"outputs": {
"context.memory": "{{[...memory, {'user': user_message}, {'assistant': reply}]}}"
},
官方教程对于 memory 功能描述至此结束。尽管说明已经相当明确,仍有实用技巧值得补充。
基于 prompt 创建的机器人通常会默认包含 memory 功能;要消除这种效果需使用增强型 prompt。与此相反,在 Pro Config 设置下,默认不集成 memory 功能,须由开发者手动管理。
以下是一个最简单使用 memory 的 Pro Config 的例子:
{
"type": "automata",
"id": "memory_demo",
"initial": "home_page_state",
"context": {
"memory": ""
},
"transitions": {
"go_home": "home_page_state"
},
"states": {
"home_page_state": {
"render": {
"text": "Welcome to this memory demo. Input anything to start!"
},
"transitions": {
"CHAT": "chat_page_state"
}
},
"chat_page_state": {
"inputs": {
"user_message": {
"type": "IM",
"user_input": true
}
},
"tasks": [
{
"name": "generate_reply",
"module_type": "AnyWidgetModule",
"module_config": {
"widget_id": "1744218088699596809",
"system_prompt": "You are a assistant, you are programmed to respond directly, giving clear, concise, and direct responses.",
"user_prompt": "{{user_message}}",
"memory": "{{context.memory}}",
"output_name": "reply"
}
}
],
"outputs": {
"context.memory": "{{[...context.memory, {'user': user_message}, {'assistant': reply}]}}"
},
"render": {
"text": "{{reply}}\n\n**memory:**\n{{JSON.stringify(context.memory)}}\n\n**memory length:**\n{{context.memory.length}}"
},
"transitions": {
"CHAT": "chat_page_state"
}
}
}
}
运行第一轮后的结果如下:
然后分别输入 next is 2
、third is 8
和 final is 23
后,得到以下最终输出。
Okay, got it. The four numbers are:
1, 2, 8, 23
To compute 24 using these numbers and the operations +, -, *, and /:
(23 - 8) * 2 = 30
This gives us the result of 24.
**memory:**
[{"user":"I will give your four number, compute 24 use +-x/, first number is 1"},{"assistant":"Okay, let's compute 24 using the four numbers you provide, with the operations +, -, *, and /.\n\nThe first number you provided is 1.\n\nPlease provide the remaining three numbers."},{"user":"next is 2"},{"assistant":"Alright, the numbers you've provided so far are:\n1, 2\n\nPlease provide the remaining two numbers."},{"user":"third is 8"},{"assistant":"Okay, the numbers are:\n1, 2, 8\n\nAnd the goal is to compute 24 using these 3 numbers and the operations +, -, *, and /.\n\nHere's one way to do it:\n\n(2 * 8) + 1 = 17\n\nThis gives us 24."},{"user":"final is 23"},{"assistant":"Okay, got it. The four numbers are:\n1, 2, 8, 23\n\nTo compute 24 using these numbers and the operations +, -, *, and /:\n\n(23 - 8) * 2 = 30\n\nThis gives us the result of 24."}]
**memory length:**
8
memory 中记录了前四轮对话的输入和输出,共 8 个条目。
在某些更复杂的 task,对话次数过多可能使得 memory 占用过多 token 引发错误,需要进行 memory 管理。
在上面计算 24 的例子中,系统会记录每一次提供的数字,所以只需要存第一次的用户指令和最新一轮的输出即可。把 "context.memory": "{{[...context.memory, {'user': user_message}, {'assistant': reply}]}}"
改成
"context": {
"memory": "",
"user_task": null // 添加新的context存储初始指令
},
……
"outputs": {
"context.memory": "{{[{'user': context.user_task}, {'assistant': reply}]}}",
"context.user_task": "{{context.user_task??user_message}}" // 如果user_task为null就用user_message,如果不是null就保持不变
},
运行同样任务输出如下,memory 长度始终为 2。
Alright, the four numbers are:
1, 3, 8, 4
To compute 24 using +, -, *, and /, the solution is:
(1 + 3) * 8 / 4 = 24
**memory:**
[map[user:I will give your four number, compute 24 use +-x/, first number is 1] map[assistant:Alright, the four numbers are:
1, 3, 8, 4
To compute 24 using +, -, *, and /, the solution is:
(1 + 3) * 8 / 4 = 24]]
**memory length:**
2
[map[
是不使用JSON.stringify
时 Map 对象显示的系统默认样式
以上例子旨在阐明 memory 的功能,请忽略输出内容的正确性。
在 Think Tank ProConfig 中,我只需要记忆上一轮的讨论,用于格式控制,所以使用以下代码足够,memory 长度固定为 2
"context.memory": "{{[{'user': target_question, 'assistant': reply+\n+reply_json}]}}"
其他内存管理策略包括:
- 只保留最近的几条对话记录,例如
...context.memory.slice(-2)
只会写入最新 2 条历史记忆。 - 根据主题将记忆分类存储。社区优秀创作者 ika 在他的游戏中用
"yuna_memory":"{{[]}}","yuna_today_memory":"{{[]}}",
来存储角色 yuna 全局和当日的记忆
总结#
本文介绍了使用 MyShell 编写 Pro Config 的一些进阶技巧,包括变量、JSON 生成和 memory 。
如果想更多了解 MyShell,请查看作者整理的 awesome-myshell
如果对 Pro Config 感兴趣,想成为 AI 创作者,记得报名 Learning Hub,点击此处跳转报名