妙笔API最佳实践

更新时间:
复制 MD 格式

本文提供妙笔写作链路 API的几个最佳实践,帮助您快速入门并开发您自己的业务应用。

前提条件

<dependency>
  <groupId>com.aliyun</groupId>
  <artifactId>alibabacloud-aimiaobi20230801</artifactId>
  <version>1.0.103</version>
</dependency>

1、直接生成文章

基于妙笔提供的API,本节通过直接生成文章场景来帮助您熟悉API的使用。

生成调用demo如下:

  • Java:

package org.example.writing;

import com.alibaba.fastjson2.JSONObject;
import com.aliyun.auth.credentials.Credential;
import com.aliyun.auth.credentials.provider.StaticCredentialProvider;
import com.aliyun.sdk.gateway.pop.Configuration;
import com.aliyun.sdk.gateway.pop.auth.SignatureVersion;
import com.aliyun.sdk.service.aimiaobi20230801.AsyncClient;
import com.aliyun.sdk.service.aimiaobi20230801.models.*;
import darabonba.core.ResponseIterable;
import darabonba.core.client.ClientOverrideConfiguration;

import java.util.List;

public class RunWritingV2DirectWritingDemo {
    private static final String ALIBABA_CLOUD_ACCESS_KEY_ID = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
    private static final String ALIBABA_CLOUD_ACCESS_KEY_SECRET = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
    private static final String WORKSPACE_ID = System.getenv("WORKSPACE_ID");
    private static final String ENDPOINT = System.getenv().getOrDefault("DOMAIN", "aimiaobi.cn-beijing.aliyuncs.com");

    public static AsyncClient asyncClient() {
        return AsyncClient.builder().credentialsProvider(
                StaticCredentialProvider.create(Credential.builder().accessKeyId(ALIBABA_CLOUD_ACCESS_KEY_ID).accessKeySecret(ALIBABA_CLOUD_ACCESS_KEY_SECRET).build())
        ).serviceConfiguration(Configuration.create().setSignatureVersion(SignatureVersion.V3)).overrideConfiguration(
                ClientOverrideConfiguration.create().setProtocol("HTTPS").setEndpointOverride(ENDPOINT)
        ).build();
    }

    public static void main(String[] args) {
        directWriting();
    }

    public static void directWriting() {
        String prompt = "写一个关于机器学习的文章";
        RunWritingV2Request.Builder builder = RunWritingV2Request.builder().workspaceId(WORKSPACE_ID);
        //启用联网溯源与溯源
        builder.sourceTraceMethod("modelSourceTrace").useSearch(true);
        builder.prompt(prompt);

        AsyncClient client = asyncClient();

        ResponseIterable<RunWritingV2ResponseBody> runWritingV2ResponseBodies = client.runWritingV2WithResponseIterable(builder.build());
        GenerateTraceability generateTraceability = null;
        for (RunWritingV2ResponseBody item : runWritingV2ResponseBodies) {
            System.out.println(JSONObject.toJSONString(item));
            if (item.getHeader().getErrorMessage() != null) {
                System.out.println("写作失败:" + item.getHeader().getErrorMessage());
                break;
            }
            if ("task-progress-start-generating".equals(item.getHeader().getEvent())) {
                System.out.println(item.getPayload().getOutput().getText());
            }

            generateTraceability = item.getPayload().getOutput().getGenerateTraceability();
        }
        if (generateTraceability != null) {
            //模型实际引用的文章列表
            List<GenerateTraceability.News> news = generateTraceability.getNews();
            System.out.println(
                    JSONObject.toJSONString(news)
            );
        }
    }

}
  • Python:

import os
from alibabacloud_tea_openapi_sse.client import Client as OpenApiClient
from alibabacloud_tea_openapi_sse import models as open_api_models
from alibabacloud_tea_util_sse import models as open_api_util_models
import asyncio
import json

endpoint = 'aimiaobi.cn-beijing.aliyuncs.com'
# 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
# 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378659.html。
access_key_id = os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_ID')

access_key_secret = os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_SECRET')

workspace_id = os.environ.get('WORKSPACE_ID')


# 详情接口文档请参考:https://api.aliyun.com/api/AiMiaoBi/2023-08-01/RunWritingV2?spm=a2c4g.11186623.0.0.201c715eKrroz5&RegionId=cn-beijing

def _create_client() -> OpenApiClient:
    config = open_api_models.Config(
        access_key_id=access_key_id,
        access_key_secret=access_key_secret,
        endpoint=endpoint
    )
    return OpenApiClient(config)


def create_sse_api_info(action) -> open_api_models.Params:
    """
    API 相关
    @param path: params
    @return: OpenApi.Params
    """
    params = open_api_models.Params(
        # 接口名称
        action=action,
        # 接口版本
        version='2023-08-01',
        # 接口协议
        protocol='HTTPS',
        # 接口 HTTP 方法
        method='POST',
        auth_type='AK',
        style='RPC',
        pathname="",
        # 接口请求体内容格式,
        req_body_type='formData',
        # 接口响应体内容格式,
        body_type='sse'
    )
    return params


class SseClient:
    def __init__(self) -> None:
        # 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
        # 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378659.html。
        self.endpoint = endpoint
        self._runtime = open_api_util_models.RuntimeOptions(read_timeout=1000 * 100)
        self._client = _create_client()

    async def run_writing(self, body):
        request = open_api_models.OpenApiRequest(body=body)
        return self._client.call_sse_api_async(params=create_sse_api_info("RunWritingV2"), request=request,
                                               runtime=self._runtime)


async def test_run_writing():
    client = SseClient()
    # 直接写作
    writing_params = {
        "Prompt": "写一个关于机器学习的文章",
        "UseSearch": True,
        "SourceTraceMethod": "modelSourceTrace",
        "WorkspaceId": workspace_id
    }
    async for item in await  client.run_writing(writing_params):
        try:
            data = json.loads(item.get('event').data)
            print(json.dumps(data, ensure_ascii=False))
        except json.JSONDecodeError as e:
            print(e)
            print('------json.JSONDecodeError--------')


if __name__ == '__main__':
    asyncio.run(test_run_writing())

2、使用模板生成文章

package org.example.writing;

import com.alibaba.fastjson2.JSONObject;
import com.aliyun.auth.credentials.Credential;
import com.aliyun.auth.credentials.provider.StaticCredentialProvider;
import com.aliyun.sdk.gateway.pop.Configuration;
import com.aliyun.sdk.gateway.pop.auth.SignatureVersion;
import com.aliyun.sdk.service.aimiaobi20230801.AsyncClient;
import com.aliyun.sdk.service.aimiaobi20230801.models.GenerateTraceability;
import com.aliyun.sdk.service.aimiaobi20230801.models.RunWritingV2Request;
import com.aliyun.sdk.service.aimiaobi20230801.models.RunWritingV2ResponseBody;
import darabonba.core.ResponseIterable;
import darabonba.core.client.ClientOverrideConfiguration;
import lombok.Data;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class RunWritingV2WithTemplate {
    private static final String ALIBABA_CLOUD_ACCESS_KEY_ID = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
    private static final String ALIBABA_CLOUD_ACCESS_KEY_SECRET = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
    private static final String WORKSPACE_ID = System.getenv("WORKSPACE_ID");
    private static final String ENDPOINT = System.getenv().getOrDefault("DOMAIN", "aimiaobi.cn-beijing.aliyuncs.com");

    public static AsyncClient asyncClient() {
        return AsyncClient.builder().credentialsProvider(
                StaticCredentialProvider.create(Credential.builder().accessKeyId(ALIBABA_CLOUD_ACCESS_KEY_ID).accessKeySecret(ALIBABA_CLOUD_ACCESS_KEY_SECRET).build())
        ).serviceConfiguration(Configuration.create().setSignatureVersion(SignatureVersion.V3)).overrideConfiguration(
                ClientOverrideConfiguration.create().setProtocol("HTTPS").setEndpointOverride(ENDPOINT)
        ).build();
    }

    public static void main(String[] args) {
        WritingResult result = templateWriting();
        // 最终汇总输出(与 RunWritingV2Outline.java / RunWritingV2WithTemplate.py 对齐)
        System.out.println("==================== 【写作结果】 ====================");
        System.out.println(JSONObject.toJSONString(result));
    }

    /**
     * 模板写作 - 传媒-新闻报道。
     *
     * <p>使用 {@code promptMode=Template},并通过 {@code writingParams} 传入:</p>
     * <ul>
     *   <li>{@code topic}           :选题</li>
     *   <li>{@code corePerspective} :核心观点</li>
     *   <li>{@code newsStructure}   :新闻结构(如「编年体结构」)</li>
     * </ul>
     *
     * <p>同时启用联网检索与模型溯源({@code modelSourceTrace})。</p>
     *
     * @return 写作过程中收集的结果({@link WritingResult}),结构与
     *         {@link RunWritingV2Outline.WritingResult} 对齐(模板写作不涉及大纲,故 outlines 为 null)。
     */
    public static WritingResult templateWriting() {
        AsyncClient client = asyncClient();

        //更多文体模板请参考接口:https://help.aliyun.com/zh/model-studio/api-aimiaobi-2023-08-01-listwritingstyles
        String writingScene = "media";
        String writingStyle = "新闻报道";


        // 模板字段(writingParams)
        Map<String, String> writingParams = new HashMap<>();
        writingParams.put("topic", "嫦娥六号实现人类首次月球背面采样返回");
        writingParams.put("corePerspective", "2024年6月25日,嫦娥六号返回器在内蒙古四子王旗预定区域成功着陆,"
                + "携带约1935.3克月球背面样品返回地球,实现人类历史上首次月背采样返回。"
                + "任务历时53天,先后完成发射入轨、地月转移、近月制动、环月飞行、着陆采样、月背起飞、"
                + "月球轨道交会对接、月地转移与再入回收等关键环节。这一里程碑式成就,"
                + "展现了我国航天科技自立自强的硬核实力,为国际月球与深空探测合作贡献了中国智慧与中国方案,"
                + "鼓舞了全国人民攀登科技高峰的信心与斗志。");
        writingParams.put("newsStructure", "编年体结构");

        RunWritingV2Request.Builder builder = RunWritingV2Request.builder()
                .workspaceId(WORKSPACE_ID)
                .step("Writing")
                .promptMode("Template")
                .writingScene(writingScene)
                .writingStyle(writingStyle)
                .writingParams(writingParams)
                .useSearch(true)
                .sourceTraceMethod("modelSourceTrace");

        // 与 RunWritingV2Outline.java 中 WritingResult 对齐的过程参数收集器
        // (模板写作不涉及大纲,故 outlines 字段为 null)
        WritingResult result = new WritingResult();

        ResponseIterable<RunWritingV2ResponseBody> stream = client.runWritingV2WithResponseIterable(builder.build());
        for (RunWritingV2ResponseBody item : stream) {
            // 打印原始事件体
            System.out.println(JSONObject.toJSONString(item));

            // 任务失败:打印错误并中断
            if (item.getHeader().getErrorMessage() != null) {
                System.err.println("ErrorCode: " + item.getHeader().getErrorCode()
                        + ", ErrorMessage: " + item.getHeader().getErrorMessage());
                break;
            }

            RunWritingV2ResponseBody.Output output = item.getPayload().getOutput();
            if (output == null) {
                continue;
            }

            // 正文增量:随流式事件持续覆盖,最终保留最后一条
            if (output.getText() != null && !output.getText().isEmpty()) {
                result.setText(output.getText());
            }

            // 溯源:任意阶段都可能携带,命中时刷新
            GenerateTraceability traceability = output.getGenerateTraceability();
            if (traceability != null && traceability.getNews() != null) {
                result.setNews(traceability.getNews());
            }
        }
        return result;
    }

    // ============================== 数据结构 ==============================

    /**
     * 模板写作的中间结果。
     *
     * <p>结构与 {@link RunWritingV2Outline.WritingResult} 对齐,便于上层统一消费;
     * 模板写作不涉及大纲,故未包含 outlines 字段。</p>
     */
    @Data
    public static class WritingResult {
        /**
         * 最终生成的正文(全量文本)。
         * <p>随流式事件持续覆盖,最终保留最后一条。</p>
         */
        private String text;

        /**
         * 溯源信息:正文中引用的文章列表。
         * <p>仅在启用溯源(如 {@code sourceTraceMethod=modelSourceTrace})且有命中时不为空。</p>
         * <p>文章与正文中的 {@code [[n]]} 标记一一对应(索引从 1 开始)。</p>
         */
        private List<GenerateTraceability.News> news;
    }

}

"""
模板写作示例(Python)。

场景:传媒-新闻报道
  使用 promptMode=Template,并通过 writingParams 传入选题/核心观点/新闻结构等模板字段,
  开启联网检索与模型溯源(modelSourceTrace)。

接口文档:
  https://api.aliyun.com/api/AiMiaoBi/2023-08-01/RunWritingV2
"""
import asyncio
import json
import os
from typing import Any, Dict

from alibabacloud_tea_openapi_sse import models as open_api_models
from alibabacloud_tea_openapi_sse.client import Client as OpenApiClient
from alibabacloud_tea_util_sse import models as open_api_util_models

# ============================== 环境配置 ==============================
endpoint = os.environ.get("DOMAIN", "aimiaobi.cn-beijing.aliyuncs.com")
# 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
# 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378659.html。
access_key_id = os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_ID")
access_key_secret = os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
workspace_id = os.environ.get("WORKSPACE_ID")


# ============================== 客户端 ==============================
def _create_client() -> OpenApiClient:
    config = open_api_models.Config(
        access_key_id=access_key_id,
        access_key_secret=access_key_secret,
        endpoint=endpoint,
    )
    return OpenApiClient(config)


def _create_sse_api_info(action: str) -> open_api_models.Params:
    return open_api_models.Params(
        action=action,
        version="2023-08-01",
        protocol="HTTPS",
        method="POST",
        auth_type="AK",
        style="RPC",
        pathname="",
        req_body_type="formData",
        body_type="sse",
    )


class SseClient:
    def __init__(self) -> None:
        self.endpoint = endpoint
        self._runtime = open_api_util_models.RuntimeOptions(read_timeout=1000 * 100)
        self._client = _create_client()

    async def run_writing(self, body: Dict[str, Any]):
        request = open_api_models.OpenApiRequest(body=body)
        return self._client.call_sse_api_async(
            params=_create_sse_api_info("RunWritingV2"),
            request=request,
            runtime=self._runtime,
        )


# ============================== 模板写作 ==============================
async def template_writing() -> Dict[str, Any]:
    """
    模板写作 - 传媒-新闻报道。

    使用 PromptMode=Template,并通过 WritingParams 传入:
      - topic           :选题
      - corePerspective :核心观点
      - newsStructure   :新闻结构(如「编年体结构」)

    :return: 写作过程中收集的结果,结构与 RunWritingV2OutlineWriting.py 对齐:
        {
          "text": "...",   # 最终生成的正文(随流式事件持续覆盖,最终保留最后一条)
          "news": [...]     # 溯源命中的引用文章列表
        }
    """
    client = SseClient()
    # 更多文体模板请参考接口:https://help.aliyun.com/zh/model-studio/api-aimiaobi-2023-08-01-listwritingstyles
    writing_scene = "media"
    writing_style = "新闻报道"

    # 模板字段(writingParams)
    writing_params: Dict[str, str] = {
        "topic": "嫦娥六号实现人类首次月球背面采样返回",
        "corePerspective": (
            "2024年6月25日,嫦娥六号返回器在内蒙古四子王旗预定区域成功着陆,"
            "携带约1935.3克月球背面样品返回地球,实现人类历史上首次月背采样返回。"
            "任务历时53天,先后完成发射入轨、地月转移、近月制动、环月飞行、着陆采样、月背起飞、"
            "月球轨道交会对接、月地转移与再入回收等关键环节。这一里程碑式成就,"
            "展现了我国航天科技自立自强的硬核实力,为国际月球与深空探测合作贡献了中国智慧与中国方案,"
            "鼓舞了全国人民攀登科技高峰的信心与斗志。"
        ),
        "newsStructure": "编年体结构",
    }

    body: Dict[str, Any] = {
        "WorkspaceId": workspace_id,
        "Step": "Writing",
        "PromptMode": "Template",
        "WritingScene": writing_scene,
        "WritingStyle": writing_style,
        "WritingParams": json.dumps(writing_params, ensure_ascii=False),
        # 启用联网检索
        "UseSearch": True,
        # 溯源方式:modelSourceTrace —— 模型在正文片段尾部输出 [[n]] 引用标记,索引从 1 开始
        "SourceTraceMethod": "modelSourceTrace",
    }

    # 与 RunWritingV2OutlineWriting.py 中 WritingResult 对齐的过程参数收集器
    # (模板写作不涉及大纲,故不包含 outlines 字段)
    result: Dict[str, Any] = {"text": None, "news": None}

    async for item in await client.run_writing(body):
        try:
            data = json.loads(item.get("event").data)
        except (json.JSONDecodeError, AttributeError) as e:
            print(f"[解析失败] {e}")
            continue

        # 打印原始事件体(与 Java 版 JSONObject.toJSONString(item) 对齐)
        print(json.dumps(data, ensure_ascii=False))

        header = data.get("Header", {}) or {}
        if header.get("ErrorMessage"):
            print(f"写作失败:{header.get('ErrorMessage')}")
            break

        output = (data.get("Payload", {}) or {}).get("Output", {}) or {}

        # 正文增量:随流式事件持续覆盖,最终保留最后一条
        text = output.get("Text")
        if text:
            result["text"] = text

        # 溯源:任意阶段都可能携带,命中时刷新
        traceability = output.get("GenerateTraceability")
        if traceability and traceability.get("News"):
            result["news"] = traceability.get("News")

    # 最终汇总输出
    print("==================== 【写作结果】 ====================")
    print(json.dumps(result, ensure_ascii=False))
    return result


if __name__ == "__main__":
    asyncio.run(template_writing())

3、分大纲生成文章

package org.example.writing;

import com.alibaba.fastjson2.JSONObject;
import com.aliyun.auth.credentials.Credential;
import com.aliyun.auth.credentials.provider.StaticCredentialProvider;
import com.aliyun.sdk.gateway.pop.Configuration;
import com.aliyun.sdk.gateway.pop.auth.SignatureVersion;
import com.aliyun.sdk.service.aimiaobi20230801.AsyncClient;
import com.aliyun.sdk.service.aimiaobi20230801.models.*;
import darabonba.core.ResponseIterable;
import darabonba.core.client.ClientOverrideConfiguration;
import lombok.Data;

import java.util.List;
import java.util.stream.Collectors;

/**
 * 分步骤大纲写作示例(精简版)。
 *
 * <p>核心流程(两步走):</p>
 * <ol>
 *   <li><b>OutlineGenerate</b>:根据 prompt 生成大纲(树形结构)。</li>
 *   <li><b>Writing</b>:基于上一步生成的大纲,分段检索并写正文。</li>
 * </ol>
 */
public class RunWritingV2Outline {

    // ============================== 环境配置 ==============================
    private static final String ALIBABA_CLOUD_ACCESS_KEY_ID = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
    private static final String ALIBABA_CLOUD_ACCESS_KEY_SECRET = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
    private static final String WORKSPACE_ID = System.getenv("WORKSPACE_ID");
    private static final String ENDPOINT = System.getenv().getOrDefault("DOMAIN", "aimiaobi.cn-beijing.aliyuncs.com");

    // ============================== 步骤 / 事件常量 ==============================
    private static final String STEP_OUTLINE_GENERATE = "OutlineGenerate";
    private static final String STEP_WRITING = "Writing";

    private static final String EVT_TASK_FAILED = "task-failed";
    private static final String EVT_OUTLINE_GENERATE = "task-outline-generate";
    private static final String EVT_OUTLINE_GENERATE_REASONING = "task-outline-generate-reasoning";
    private static final String EVT_OUTLINE_GENERATE_END = "task-outline-generate-end";
    private static final String EVT_OUTLINE_SEARCH_START = "task-outline-search-start";
    private static final String EVT_OUTLINE_SEARCH = "task-outline-search";

    // 大纲写作专用固定值
    private static final String WRITING_STYLE_OUTLINE = "outlineWriting";
    private static final String WRITING_SCENE_OTHERS = "others";

    // ============================== 客户端 ==============================
    public static AsyncClient asyncClient() {
        return AsyncClient.builder()
                .credentialsProvider(StaticCredentialProvider.create(
                        Credential.builder()
                                .accessKeyId(ALIBABA_CLOUD_ACCESS_KEY_ID)
                                .accessKeySecret(ALIBABA_CLOUD_ACCESS_KEY_SECRET)
                                .build()))
                .serviceConfiguration(Configuration.create().setSignatureVersion(SignatureVersion.V3))
                .overrideConfiguration(ClientOverrideConfiguration.create()
                        .setProtocol("HTTPS")
                        .setEndpointOverride(ENDPOINT))
                .build();
    }

    // ============================== 入口:全链路 ==============================
    public static void main(String[] args) {
        AsyncClient client = asyncClient();
        String prompt = "写一篇关于新能源汽车的文章,3000字左右";

        // 步骤一:生成大纲
        System.out.println("==================== 【步骤一】生成大纲 ====================");
        WritingResult outlineResult =  runWriting(client, prompt, STEP_OUTLINE_GENERATE, null);

        if (outlineResult.getOutlines() == null || outlineResult.getOutlines().isEmpty()) {
            System.err.println("未获取到大纲,跳过正文写作。");
            return;
        }
        System.out.println("大纲预览:");
        for (WritingOutline outline : outlineResult.getOutlines()) {
            System.out.print(outlineToTreeStr(outline, 0));
        }

        // 步骤二:基于大纲写正文
        System.out.println("==================== 【步骤二】写正文 ====================");
        WritingResult writingResult =  runWriting(client, prompt, STEP_WRITING, outlineResult.getOutlines());
        System.out.println(JSONObject.toJSONString(writingResult));
    }

    // ============================== 核心调用 ==============================

    private static WritingResult runWriting(AsyncClient client,
                                            String prompt,
                                            String step,
                                            List<WritingOutline> outlines) {
        RunWritingV2Request request = buildRequest(prompt, step, outlines);
        ResponseIterable<RunWritingV2ResponseBody> stream = client.runWritingV2WithResponseIterable(request);

        WritingResult result = new WritingResult();
        for (RunWritingV2ResponseBody item : stream) {

            System.out.println(JSONObject.toJSONString(item));

            String event = item.getHeader().getEvent();
            RunWritingV2ResponseBody.Output output = item.getPayload().getOutput();

            // 任务失败:打印错误并中断
            if (EVT_TASK_FAILED.equals(event)) {
                System.err.println("ErrorCode: " + item.getHeader().getErrorCode()
                        + ", ErrorMessage: " + item.getHeader().getErrorMessage());
                break;
            }
            // 大纲事件:刷新大纲
            else if (isOutlineGenerateEvent(event)) {
                if (EVT_OUTLINE_GENERATE_REASONING.equals(event)) {
                    System.out.println("[大纲推理] " + output.getText());
                } else {
                    result.setOutlines(output.getOutlines());
                }
            }
            // 大纲检索事件
            else if (isOutlineSearchEvent(event)) {
                printOutlineSearch(event, output);
            }
            // 正文事件
            else {
                System.out.println("[正文片段] " + JSONObject.toJSONString(output.getText()));
                result.setText(output.getText());
            }
            if ( output.getGenerateTraceability()!=null ){
                //溯源
                result.setNews(output.getGenerateTraceability().getNews());
            }
        }
        return result;
    }

    private static RunWritingV2Request buildRequest(String prompt, String step,
                                                    List<WritingOutline> outlines) {
        RunWritingV2Request.Builder builder = RunWritingV2Request.builder()
                .workspaceId(WORKSPACE_ID)
                // 大纲写作固定使用 outlineWriting + others
                .writingStyle(WRITING_STYLE_OUTLINE)
                .writingScene(WRITING_SCENE_OTHERS)
                .sourceTraceMethod("modelSourceTrace")
                // 联网检索
                .useSearch(true)
                .step(step);

        if (prompt != null) {
            builder.prompt(prompt);
        }
        if (outlines != null) {
            builder.outlineList(outlines);
        }
        return builder.build();
    }

    // ============================== 事件辅助 ==============================

    private static boolean isOutlineGenerateEvent(String event) {
        return EVT_OUTLINE_GENERATE.equals(event)
                || EVT_OUTLINE_GENERATE_REASONING.equals(event)
                || EVT_OUTLINE_GENERATE_END.equals(event);
    }

    private static boolean isOutlineSearchEvent(String event) {
        return EVT_OUTLINE_SEARCH_START.equals(event) || EVT_OUTLINE_SEARCH.equals(event);
    }

    /** 简单打印大纲检索的开始与命中信息 */
    private static void printOutlineSearch(String event, RunWritingV2ResponseBody.Output output) {
        if (EVT_OUTLINE_SEARCH_START.equals(event)) {
            System.out.println("[大纲检索·开始] " + output.getText());
            return;
        }
        OutlineSearchResult sr = output.getSearchResult();
        if (sr == null || sr.getArticles() == null) {
            return;
        }
        System.out.println("[大纲检索·命中] " + sr.getOutline()
                + " (Query=" + sr.getQuery() + ", 共 " + sr.getArticles().size() + " 篇): "
                + sr.getArticles().stream().map(OutlineWritingArticle::getTitle)
                        .collect(Collectors.joining(", ")));
    }

    // ============================== 工具方法 ==============================

    /** 将单个大纲转换为树形文本结构(便于在控制台直观查看大纲结构)。 */
    public static String outlineToTreeStr(WritingOutline outline, int level) {
        StringBuilder sb = new StringBuilder();
        StringBuilder indent = new StringBuilder();
        for (int i = 0; i < level; i++) {
            indent.append("  ");
        }

        if (outline.getOutline() != null && !outline.getOutline().trim().isEmpty()) {
            String symbol = (level == 0) ? "┌─ " : "├─ ";
            sb.append(indent).append(symbol).append(outline.getOutline()).append("\n");
        }
        if (outline.getChildren() != null) {
            for (WritingOutline child : outline.getChildren()) {
                sb.append(outlineToTreeStr(child, level + 1));
            }
        }
        return sb.toString();
    }

    // ============================== 数据结构 ==============================

    /** 大纲写作的中间结果。 */
    @Data
    public static class WritingResult {
        /**
         * 大纲树形结构。
         * <p>仅在 {@code OutlineGenerate} 阶段会被刷新;在 {@code Writing} 阶段为 {@code null}。</p>
         * <p>可作为下一步「正文写作」的 {@code outlineList} 参数传入。</p>
         */
        private List<WritingOutline> outlines;

        /**
         * 最终生成的正文(全量文本)。
         * <p>仅在 {@code Writing} 阶段会被赋值;随流式事件持续覆盖,最终保留最后一条。</p>
         */
        private String text;

        /**
         * 溯源信息:正文中引用的文章列表。
         * <p>仅在启用溯源(如 {@code sourceTraceMethod=modelSourceTrace})且有命中时不为空。</p>
         * <p>文章与正文中的 {@code [[n]]} 标记一一对应(索引从 1 开始)。</p>
         */
        private java.util.List<GenerateTraceability.News> news;
    }
}

"""
分步骤大纲写作示例(Python)。

核心流程(两步走):
  1) OutlineGenerate:根据 prompt 生成大纲(树形结构)
  2) Writing       :基于上一步生成的大纲,分段检索并写正文

接口文档:
  https://api.aliyun.com/api/AiMiaoBi/2023-08-01/RunWritingV2
"""
import asyncio
import json
import os
from typing import Any, Dict, List, Optional

from alibabacloud_tea_openapi_sse import models as open_api_models
from alibabacloud_tea_openapi_sse.client import Client as OpenApiClient
from alibabacloud_tea_util_sse import models as open_api_util_models

# ============================== 环境配置 ==============================
endpoint = os.environ.get("DOMAIN", "aimiaobi.cn-beijing.aliyuncs.com")
# 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
# 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378659.html。
access_key_id = os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_ID")
access_key_secret = os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
workspace_id = os.environ.get("WORKSPACE_ID")

# ============================== 步骤 / 事件常量 ==============================
STEP_OUTLINE_GENERATE = "OutlineGenerate"
STEP_WRITING = "Writing"

EVT_TASK_FAILED = "task-failed"
EVT_OUTLINE_GENERATE = "task-outline-generate"
EVT_OUTLINE_GENERATE_REASONING = "task-outline-generate-reasoning"
EVT_OUTLINE_GENERATE_END = "task-outline-generate-end"
EVT_OUTLINE_SEARCH_START = "task-outline-search-start"
EVT_OUTLINE_SEARCH = "task-outline-search"

# 大纲写作专用固定值
WRITING_STYLE_OUTLINE = "outlineWriting"
WRITING_SCENE_OTHERS = "others"


# ============================== 客户端 ==============================
def _create_client() -> OpenApiClient:
    config = open_api_models.Config(
        access_key_id=access_key_id,
        access_key_secret=access_key_secret,
        endpoint=endpoint,
    )
    return OpenApiClient(config)


def _create_sse_api_info(action: str) -> open_api_models.Params:
    return open_api_models.Params(
        action=action,
        version="2023-08-01",
        protocol="HTTPS",
        method="POST",
        auth_type="AK",
        style="RPC",
        pathname="",
        req_body_type="formData",
        body_type="sse",
    )


class SseClient:
    def __init__(self) -> None:
        self.endpoint = endpoint
        self._runtime = open_api_util_models.RuntimeOptions(read_timeout=1000 * 100)
        self._client = _create_client()

    async def run_writing(self, body: Dict[str, Any]):
        request = open_api_models.OpenApiRequest(body=body)
        return self._client.call_sse_api_async(
            params=_create_sse_api_info("RunWritingV2"),
            request=request,
            runtime=self._runtime,
        )


# ============================== 工具方法 ==============================
def outline_to_tree_str(outline: Dict[str, Any], level: int = 0) -> str:
    """将单个大纲转为树形文本,便于在控制台直观查看大纲结构。"""
    indent = "  " * level
    text = ""
    name = (outline.get("Outline") or outline.get("outline") or "").strip()
    if name:
        symbol = "┌─ " if level == 0 else "├─ "
        text += f"{indent}{symbol}{name}\n"
    children = outline.get("Children") or outline.get("children") or []
    for child in children:
        text += outline_to_tree_str(child, level + 1)
    return text


def _is_outline_generate_event(event: str) -> bool:
    return event in (
        EVT_OUTLINE_GENERATE,
        EVT_OUTLINE_GENERATE_REASONING,
        EVT_OUTLINE_GENERATE_END,
    )


def _is_outline_search_event(event: str) -> bool:
    return event in (EVT_OUTLINE_SEARCH_START, EVT_OUTLINE_SEARCH)


def _print_outline_search(event: str, output: Dict[str, Any]) -> None:
    if event == EVT_OUTLINE_SEARCH_START:
        print(f"[大纲检索·开始] {output.get('Text', '')}")
        return
    sr = output.get("SearchResult")
    if not sr:
        return
    articles = sr.get("Articles") or []
    titles = ", ".join(a.get("Title", "") for a in articles)
    print(
        f"[大纲检索·命中] {sr.get('Outline', '')} "
        f"(Query={sr.get('Query', '')}, 共 {len(articles)} 篇): {titles}"
    )


# ============================== 核心调用 ==============================
def _build_body(
        prompt: Optional[str],
        step: str,
        outlines: Optional[List[Dict[str, Any]]],
) -> Dict[str, Any]:
    body: Dict[str, Any] = {
        "WorkspaceId": workspace_id,
        # 大纲写作固定使用 outlineWriting + others
        "WritingStyle": WRITING_STYLE_OUTLINE,
        "WritingScene": WRITING_SCENE_OTHERS,
        # 联网检索
        "UseSearch": True,
        # 溯源方式:modelSourceTrace —— 模型在正文片段尾部输出 [[n]] 引用标记,索引从 1 开始
        "SourceTraceMethod": "modelSourceTrace",
        "Step": step,
    }
    if prompt is not None:
        body["Prompt"] = prompt
    if outlines is not None:
        body["OutlineList"] = outlines
    return body


async def run_writing(
        client: SseClient,
        prompt: Optional[str],
        step: str,
        outlines: Optional[List[Dict[str, Any]]] = None,
) -> Dict[str, Any]:
    """统一发起 RunWritingV2 调用,并按事件类型分流处理。

    返回结构:
      {
        "outlines": [...],   # OutlineGenerate 阶段刷新
        "text": "...",        # Writing 阶段最终正文
        "news": [...]         # 溯源命中的引用文章
      }
    """
    body = _build_body(prompt, step, outlines)
    result: Dict[str, Any] = {"outlines": None, "text": None, "news": None}

    async for item in await client.run_writing(body):
        try:
            data = json.loads(item.get("event").data)
        except (json.JSONDecodeError, AttributeError) as e:
            print(f"[解析失败] {e}")
            continue

        # 打印原始事件体(与 Java 版 JSONObject.toJSONString(item) 对齐)
        print(json.dumps(data, ensure_ascii=False))

        header = data.get("Header", {}) or {}
        payload = data.get("Payload", {}) or {}
        output = payload.get("Output", {}) or {}
        event = header.get("Event")

        # 任务失败:打印错误并中断
        if event == EVT_TASK_FAILED:
            print(
                f"[任务失败] ErrorCode={header.get('ErrorCode')}, "
                f"ErrorMessage={header.get('ErrorMessage')}"
            )
            break

        # 大纲事件:刷新大纲
        if _is_outline_generate_event(event):
            if event == EVT_OUTLINE_GENERATE_REASONING:
                print(f"[大纲推理] {output.get('Text', '')}")
            else:
                result["outlines"] = output.get("Outlines")

        # 大纲检索事件
        elif _is_outline_search_event(event):
            _print_outline_search(event, output)

        # 正文事件
        else:
            text = output.get("Text", "")
            if text:
                print(f"[正文片段] {json.dumps(text, ensure_ascii=False)}")
                result["text"] = text

        # 溯源信息
        traceability = output.get("GenerateTraceability")
        if traceability and traceability.get("News"):
            result["news"] = traceability.get("News")

    return result


# ============================== 全链路示例 ==============================
async def test_outline_writing():
    client = SseClient()
    prompt = "写一篇关于新能源汽车的文章,3000字左右"

    # 步骤一:生成大纲
    print("==================== 【步骤一】生成大纲 ====================")
    outline_result = await run_writing(client, prompt, STEP_OUTLINE_GENERATE)
    outlines = outline_result.get("outlines")
    if not outlines:
        print("未获取到大纲,跳过正文写作。")
        return

    print("大纲预览:")
    for outline in outlines:
        print(outline_to_tree_str(outline, 0), end="")

    # 步骤二:基于大纲写正文
    print("==================== 【步骤二】写正文 ====================")
    writing_result = await run_writing(client, prompt, STEP_WRITING, outlines=outlines)
    print(json.dumps(writing_result, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    asyncio.run(test_outline_writing())

4、AI工具箱

AI工具箱包括内容续写、摘要生成、标题生成、内容缩写、内容扩写、关键词抽取等功能,最新实时能力可参考妙笔写作页面的AI工具箱。本节通过实现摘要生成场景来帮助您熟悉API的使用。

生成调用demo如下:

package com.aliyun.sdk.service.aimiaobi20230801;


import com.aliyun.auth.credentials.Credential;
import com.aliyun.auth.credentials.provider.StaticCredentialProvider;
import com.aliyun.sdk.gateway.pop.Configuration;
import com.aliyun.sdk.gateway.pop.auth.SignatureVersion;
import com.aliyun.sdk.service.aimiaobi20230801.models.RunSummaryGenerateRequest;
import com.aliyun.sdk.service.aimiaobi20230801.models.RunSummaryGenerateResponseBody;
import com.aliyun.sdk.service.aimiaobi20230801.models.RunWritingRequest;
import com.aliyun.sdk.service.aimiaobi20230801.models.RunWritingResponseBody;
import com.google.gson.Gson;
import com.aliyun.sdk.service.aimiaobi20230801.AsyncClient;
import darabonba.core.ResponseIterable;
import darabonba.core.ResponseIterator;
import darabonba.core.client.ClientOverrideConfiguration;

import java.util.ArrayList;
import java.util.List;

/**
 * packageName com.dayouz.lightapp
 *
 * @author dayouz
 * @version JDK 8
 * @className runStyleWriting
 * @date 2024/8/13
 * @description 摘要生成Demo
 */
public class RunSummaryGenerateTest {

    public static void main(String[] args) {
        StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder()
                .accessKeyId(Constant.accessKeyId)
                .accessKeySecret(Constant.accessKeySecret)
                .build());

        AsyncClient client = AsyncClient.builder()
                .region("cn-beijing")
                .credentialsProvider(provider)
                .serviceConfiguration(Configuration.create().setSignatureVersion(SignatureVersion.V3)).overrideConfiguration(ClientOverrideConfiguration.create().setProtocol("HTTPS").setEndpointOverride("aimiaobi.cn-beijing.aliyuncs.com"))
                .build();


        RunSummaryGenerateRequest request = RunSummaryGenerateRequest.builder()
                .workspaceId(Constant.workspaceId)
                .prompt("请为上述内容生成一段摘要,字数在100~200字以内。")
                .content("云服务器ECS(Elastic Compute Service)是阿里云提供的性能卓越、稳定可靠、弹性扩展的IaaS(Infrastructure as a Service)级别云计算服务。云服务器ECS免去了您采购IT硬件的前期准备,让您像使用水、电、天然气等公共资源一样便捷、高效地使用服务器,实现计算资源的即开即用和弹性伸缩。阿里云ECS持续提供创新型服务器,解决多种业务需求,助力您的业务发展。")
                .build();


        ResponseIterable<RunSummaryGenerateResponseBody> x = client.runSummaryGenerateWithResponseIterable(request);

        ResponseIterator<RunSummaryGenerateResponseBody> iterator = x.iterator();
        while (iterator.hasNext()) {
            System.out.println("----event----");
            RunSummaryGenerateResponseBody event = iterator.next();
            System.out.println(new Gson().toJson(event));
        }

        System.out.println("ALL***********************");
        System.out.println("请求成功的请求头值:");
        System.out.println(x.getStatusCode());
        System.out.println(x.getHeaders());
    }
}