script (⏱,💰)

script (⏱,💰)

MyShell Pro Config 进阶技巧分享

本文面向已了解 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 ProConfig 链接

Think Tank

起初,我仅按照教程创建了 Think Tank 的 Pro Config 版来通过考核,但未理解 Pro Config 的全部功能。最近新增了推荐话题功能来促进用户与 AI 的互动。在开发新功能中,我对 Pro Config 有了更深刻的认识。以下总结出了一些官方教程未详细说明的实用技巧。

变量#

Pro Config 的变量分为全局变量和局部变量,具体在官方教程的 expressions and variables 章节中有具体阐述。

全局变量用 context 读写。适合存各种类型的数据:

  1. string:字符串,可用于保存文本类型的输出,或将 JSON 对象通过 JSON.stringify 序列化后存储。
  2. prompt: 提示词,将 system_prompt 存入 context,将需传递的变量全部置于 user_prompt。
  3. list:列表,非字符串类型应使用 {{}} 进行包裹。
  4. number: 数字,同样需要用 {{}}包裹
  5. bool:布尔值,同样需要用 {{}}包裹
  6. null: 可空值
  7. 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 将其置于按钮描述里,需鼠标悬停方可查看。

Aldea

如果 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)}}"
  },
……

输出结果:
result

更详细用法可参考 官方 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"
        }
      }
    }
  }

运行第一轮后的结果如下:

memory

然后分别输入 next is 2third 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}]}}"

 其他内存管理策略包括:

  1. 只保留最近的几条对话记录,例如 ...context.memory.slice(-2) 只会写入最新 2 条历史记忆。
  2. 根据主题将记忆分类存储。社区优秀创作者 ika 在他的游戏中用 "yuna_memory":"{{[]}}","yuna_today_memory":"{{[]}}", 来存储角色 yuna 全局和当日的记忆

总结#

本文介绍了使用 MyShell 编写 Pro Config 的一些进阶技巧,包括变量、JSON 生成和 memory 。

如果想更多了解 MyShell,请查看作者整理的 awesome-myshell

如果对 Pro Config 感兴趣,想成为 AI 创作者,记得报名 Learning Hub,点击此处跳转报名

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.