xChar

本文面向已了解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 comments...