LigaAI – LigaAI 团队博客 https://ligai.cn/blog 以人工智能,赋能项目管理 Fri, 31 May 2024 03:45:11 +0000 zh-CN hourly 1 https://wordpress.org/?v=5.8.4 https://ligai.cn/blog/wp-content/uploads/2021/02/logo_图形-150x150.png LigaAI – LigaAI 团队博客 https://ligai.cn/blog 32 32 技术分享 | SpringBoot 流式输出时,正常输出后为何突然报错? https://ligai.cn/blog/sharing/1521.html Fri, 31 May 2024 03:45:11 +0000 https://ligai.cn/blog/?p=1521 阅读更多]]>
  • 一个 SpringBoot 项目同时使用了 Tomcat 的过滤器和 Spring 的拦截器,一些线程变量在过滤器中初始化并在拦截器中使用。
  • 该项目需要调用大语言模型进行流式输出。
  • 项目中,笔者使用 SpringBoot 的 ResponseEntity<StreamingResponseBody> 将流式输出返回前端。
  • 问题出现

    问题出现在上述第 3 点:正常输出一段内容后,后台突然报错,而报错内容由拦截器产生

    笔者仔细查看了报错日志,发现只是拦截器的问题:执行时由于某些线程变量不存在而报错。但是,这些线程变量已经在过滤器中初始化了。

    那么问题来了:为什么这个接口明明可以正常通过过滤器和拦截器,并开始正常输出,却又突然在拦截器中报错呢?

    场景重现

    Filter

    @Slf4j
    @Component
    @Order(1)
    public class MyFilter implements Filter {
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            // 要继续处理请求,必须添加 filterChain.doFilter()
            log.info("doFilter method is running..., thread: {}, dispatcherType: {}", Thread.currentThread(), servletRequest.getDispatcherType()); 
            filterChain.doFilter(servletRequest,servletResponse);
        } 
    }

    Interceptor

    @Slf4j
    public class MyInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws Exception {
            log.info("preHandle method is running..., thread: {}, dispatcherType: {}", Thread.currentThread(), request.getDispatcherType());
            if (DispatcherType.ASYNC == request.getDispatcherType()) {
                log.info("preHandle dispatcherType={}", request.getDispatcherType());
            }
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            log.info("postHandle method is running..., thread: {}", Thread.currentThread());
        }      
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            log.info("afterCompletion method is running..., thread: {}", Thread.currentThread());
        } 
    }

    WebMvcConfigurer

    @Configuration
    public class WebAppConfigurer implements WebMvcConfigurer {
    
        @Bean
        public MyInterceptor myInterceptor() {
            return new MyInterceptor();
        }
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(myInterceptor()).addPathPatterns("/**");
        }
    
        @Override
        public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
            configurer.setDefaultTimeout(120_000L);
            configurer.registerCallableInterceptors();
            configurer.registerDeferredResultInterceptors();
    
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setCorePoolSize(5);
            executor.setMaxPoolSize(10);
            executor.setQueueCapacity(100);
            executor.setThreadNamePrefix("web-async-");
            executor.initialize();
            configurer.setTaskExecutor(executor);
        }
    }

    Controller

    @Slf4j
    @RestController
    @RequestMapping("/test-stream")
    public class TestStreamController {
    
        @ApiOperation("流式输出示例")
        @PostMapping(value = "/example", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
        public ResponseEntity<StreamingResponseBody> example() {
            log.info("Stream method is running, thread: {}", Thread.currentThread());
            return  ResponseEntity.status(HttpStatus.OK)
                .contentType(new MediaType(MediaType.TEXT_EVENT_STREAM, StandardCharsets.UTF_8))
                .body(outputStream -> {
                    log.info("Internal stream method is running, thread: {}", Thread.currentThread());
                    try (outputStream) {
                        String msg = "To be or not to be!";
                        outputStream.write(msg.getBytes(StandardCharsets.UTF_8));
                        outputStream.flush();
                    }
                });
        }
    }

    根据以下运行日志,我们可以看到拦截器的 preHandle 确实执行了两次,并且此次调用过程共有 3 个线程(io-14000-exec-1web-async-1io-14000-exec-2)参与了工作。

    2024-05-06 07:35:27.362  INFO 209108 --- [io-14000-exec-1] o.a.c.c.C.[.[localhost].[/java-study]    : Initializing Spring DispatcherServlet 'dispatcherServlet'
    2024-05-06 07:35:27.362  INFO 209108 --- [io-14000-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
    2024-05-06 07:35:27.365  INFO 209108 --- [io-14000-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 3 ms
    2024-05-06 07:35:27.402  INFO 209108 --- [io-14000-exec-1] com.peng.java.study.web.config.MyFilter  : doFilter method is running..., thread: Thread[http-nio-14000-exec-1,5,main], dispatcherType: REQUEST
    2024-05-06 07:35:28.107  INFO 209108 --- [io-14000-exec-1] c.p.java.study.web.config.MyInterceptor  : preHandle method is running..., thread: Thread[http-nio-14000-exec-1,5,main], dispatcherType: REQUEST
    2024-05-06 07:35:28.121  INFO 209108 --- [io-14000-exec-1] c.p.j.s.w.r.test.TestStreamController    : Stream method is running, thread: Thread[http-nio-14000-exec-1,5,main]
    2024-05-06 07:35:28.152  INFO 209108 --- [    web-async-1] c.p.j.s.w.r.test.TestStreamController    : Internal stream method is running, thread: Thread[web-async-1,5,main]
    2024-05-06 07:35:28.167  INFO 209108 --- [io-14000-exec-2] c.p.java.study.web.config.MyInterceptor  : preHandle method is running..., thread: Thread[http-nio-14000-exec-2,5,main], dispatcherType: ASYNC
    2024-05-06 07:35:28.167  INFO 209108 --- [io-14000-exec-2] c.p.java.study.web.config.MyInterceptor  : preHandle dispatcherType=ASYNC
    2024-05-06 07:35:28.174  INFO 209108 --- [io-14000-exec-2] c.p.java.study.web.config.MyInterceptor  : postHandle method is running..., thread: Thread[http-nio-14000-exec-2,5,main]
    2024-05-06 07:35:28.183  INFO 209108 --- [io-14000-exec-2] c.p.java.study.web.config.MyInterceptor  : afterCompletion method is running..., thread: Thread[http-nio-14000-exec-2,5,main]

    问题分析

    1. 方法调用流程的差异

    众所周知,SpringBoot 的普通输出接口调用流程图如图 1 所示。

    图1-SpringBoot 普通输出调用流程图

    结合日志,我们可以简单画出流式输出接口对应的流程图(图 2)。

    图2-SpringBoot 流式输出调用流程图

    2. 线程的差异

    普通接口的执行时序图如图 3 所示。

    图3-普通接口的时序图

    而流式接口的时序图如图 4 所示。

    图4-流式接口的调用时序图

    解决问题

    通过分析,对流式输出的情况提出两种解决方案:

    1. 将过滤器中的部分业务逻辑迁移到拦截器中。
    2. 根据条件,跳过第二次的拦截器 preHandle 方法。

    笔者选择了第二个方案,实现代码如下。

    @Slf4j
    public class MyInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws Exception {
            log.info("preHandle method is running..., thread: {}, dispatcherType: {}", Thread.currentThread(), request.getDispatcherType());
            // 如果是异步请求,则跳过
            if (DispatcherType.ASYNC == request.getDispatcherType()) {
                log.info("preHandle dispatcherType={}", request.getDispatcherType());
                return true;
            }
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            log.info("postHandle method is running..., thread: {}", Thread.currentThread());     
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            log.info("afterCompletion method is running..., thread: {}", Thread.currentThread());
        } 
    }

    需要注意,请求线程和回调线程都需考虑清理线程变量,不然会导致内存泄漏。


    >> LigaAI 往期精彩阅读 <<

    用 MVP(最小可行性产品) 做低成本快速验证,为什么不灵了?

    6 大原则!助你构建高绩效的研发强军 | Liga译文

    技术分享 | 弹窗开发中,如何使用 Hook 封装 el-dialog?

    LigaAI x 极狐GitLab,共探 AI 时代研发提效新范式

    欢迎试用 LigaAI-智能研发协作平台,体验智能研发协作,一起变大变强!

    ]]>
    用 MVP(最小可行性产品) 做低成本快速验证,为什么不灵了?| Liga译文 https://ligai.cn/blog/alige/1517.html Tue, 21 May 2024 02:28:09 +0000 https://ligai.cn/blog/?p=1517 阅读更多]]> 初创企业的故事大多始于一次「灵光乍现」。创始人们窥见一个没有良好解决方案的问题,于是便琢磨起: “我可以如何解决它?”

    不过很遗憾,这并不是一个正确的问题——这也是九成的初创公司走向失败的一个重要原因。据 Exploding Topics 报道,10% 的初创企业无法顺利度过第一年,而 70% 的企业也在第 2~5 年宣告失败。 最终,只有十分之一的初创企业能够幸存。

    在过去 20 年与创业者的合作中,我参与了 20 多家初创企业从创意萌生到 IPO 的全过程——能获得成功的公司少之又少。我从中学到最有用的一课,不是「企业为什么能成功」,而是它们「会被什么击败」。

    其中大部分首次创业者不够投入,往往仅凭直觉就匆忙拼凑出一个不经测试的产品,而其他人则是过于投入而忽略了市场验证和测试,产品功能要么太过单一,要么过分臃肿。

    你可能想说:“这不就是没有通过 MVP (Minimum Viable Product,最小可行性产品)进行快速学习吗?”

    ——说的很好,但也不准确。

    这个问题的关键在于「V」是什么。在商业语境下,真正重要的是 Viable 还是 Valuable?

    首先,让我们从灯泡的故事讲起。

    灯泡带来的启示

    你或许在小学时学过「爱迪生发明了电灯泡」。

    但事实并非如此。

    在爱迪生之前,包括埃比尼泽·金纳斯利(Ebenezer Kinnersley)、汉弗里·戴维 (Humphry Davy)和约瑟夫·斯万(Joseph Swan)在内的许多发明家已经发明出了各式各样的白炽灯丝。甚至在 19 世纪 70 年代,当爱迪生转向安全、可持续、经济且无异味的照明方案研究时,就已经有了一些关于白炽灯泡的专利(当时主要的室内照明方案是煤气灯)。

    作为一名创新者和企业家,爱迪生早就决定,商业变现不必依靠发明本身。他更感兴趣的是「完善」,即让事情变得更好或更便宜。 我很喜欢《纽约客》的说法:

    爱迪生从不寻找需要解决方案的问题,他所找的是需要修改的解决方案。

    当时现有的灯泡解决方案不太实用,而且灯泡的使用寿命也不长。也就是说,它们是可行的,但并不是特别有价值。

    这正是爱迪生能够脱颖而出的原因。爱迪生和他的团队在位于加州门洛帕克的总部对 3,000~6,000 种材料和灯丝进行了测试,直到 1879 年才发现碳是最佳的解决方案。一年后,他们发现碳化竹子可以燃烧超过 1,000 个小时,于是今天为人熟知的白炽灯泡诞生了。

    爱迪生如此努力创造的不是 MVP,而是 MVE(Minimum Valuable Experience)——最低价值体验

    如何将人们迫切需要的东西,变得易于获得、经济实惠且经久耐用? 这才是真正的「灵光一现」。

    A.C.T 框架

    了解如何建立 MVE 前,有两个关键点需要注意:

    • 你的故事就是策略。
    • 表达的方式非常重要,它决定了用户对产品是「无感」还是「每月复购」。

    所有的一切都是为了了解你的用户,了解什么话术和信息能够引起他们的共鸣,以及哪些策略、触点和触发条件能够刺激他们采取行动。

    为了快速高效地建立 MVE,我开发了「A.C.T 框架」。它有助于快速明确方向并节省大量无用的营销活动。A.C.T 框架由三个重要部分组成:

    • Audience 受众
    • Communication 交流
    • Touchpoints 触点

    A = 受众群体:你要对「谁」说话?

    想要知道说些什么,首先得弄清楚你要跟谁交流。

    • 谁是你的理想客户?
    • 客户考虑新的解决方案时,想要/需要/使用的是什么?
    • 在寻找新方案的过程中,他们会搜索什么?
    • 是哪些习惯、行为、目标或者决定性特征吸引了他们来使用你的产品?

    C = 交流:你要说什么?

    这是一件「用什么方式传递什么信息」的艺术。

    你必须用能够引起客户共鸣的语言和方式,用他们的方法跟他们交流,去传达那些适配他们味蕾(和钱包)的愿景或动机。

    T = 触点:在哪分享信息?

    弄清楚「和谁说」以及「说什么」之后,就可以好好想想「该怎么做」了。

    设计能刺激客户采取实际行动的触点和触发条件,包括官网展示、社媒传播和邮件营销等等。其核心目的是辨别和确定最有效且投资回报率最高的渠道和策略。

    这也是最容易出错的部分。有时候,你可能只需要一个简单的注册表单,而有的时候则需要采取更有策略性(如建立私域群组)或更复杂(如一个详细的销售漏斗和沉浸式故事)的方法。

    案例应用

    假设现在要推出一款全新的无酒精啤酒饮料,你会如何应用 A.C.T 框架回答以上问题?

    (花点时间,动动小脑瓜 )

    下面分享我的回答。别担心,问题没有标准答案,思路和分析才是重点。

    A:无酒精啤酒的理想受众是谁?

    传统人口统计特征中的年龄、性别或地理位置等数据并不能提供太多有效信息,因为无酒精啤酒很可能不在任何标签中。

    从行为角度来看,购买无酒精啤酒的人喜欢喝啤酒,但不喜欢酒精对健康、行为能力和工作效率的影响;他们富有猎奇心,足够开放,愿意尝试和接受新鲜事物。

    C:要向他们传递什么信息?

    我需要让理想客户知道,这款饮品提供了最接近啤酒的体验:它采用高品质原料,通过手工精酿制成,其炫酷的包装甚至可以媲美最流行的 IPA 啤酒。最重要的是,它的热量很低,并且不会产生任何宿醉感。

    T:在什么渠道发布和传播信息?

    无酒精啤酒代表了一种健康但仍然可以享受乐趣的生活态度,所以年轻化的短视频平台是很好的渠道。如果是线下宣传,健身房旁边的水吧或者支持多人聚餐的健康餐厅也是不错的选择。

    A.C.T 框架有助于了解「哪些体验或触点可以带来产品吸引力」。

    对某些人来说,最好的触点可能是社交媒体上某个热门话题下的一张精美表单;对其他人而言,可能是一个关于产品如何提升生活品质的巧妙故事,或者一场能够清楚传递「我们如何能帮你成为更好的自己」的精彩活动。不同的品牌和产品会有不同的答案。

    而「A.C.T + MVE」能让你快速迭代,获得最有价值的体验。当你专注于研究潜在客户真正重要的事情时,你就能点亮他们的生活。

    在我看来,这是通往商业成功的唯一可行路径。

    原文作者为 Pete Sena,内容经 LigaAI 翻译整理。


    >> LigaAI 往期精彩阅读 <<

    6 大原则!助你构建高绩效的研发强军 | Liga译文

    技术分享 | 弹窗开发中,如何使用 Hook 封装 el-dialog?

    LigaAI x 极狐GitLab,共探 AI 时代研发提效新范式

    欢迎试用 LigaAI-智能研发协作平台,体验智能研发协作,一起变大变强!

    ]]>
    技术分享 | 弹窗开发中,如何使用 Hook 封装 el-dialog? https://ligai.cn/blog/sharing/1509.html Mon, 06 May 2024 08:35:02 +0000 https://ligai.cn/blog/?p=1509 阅读更多]]> 弹窗是前端开发中的一种常见需求。Element UI 框架中的 el-dialog 组件提供了弹窗相关的基本功能,但在实际开发中,我们难免会遇到一些定制化需求,比如对弹窗进行二次封装以便在项目中统一管理样式和行为。

    本文将分享如何使用 useDialog Hook 封装 el-dialog,实现更灵活、更易用的弹窗组件。

    一、问题澄清

    「将一个通用的组件应用在多个页面」是一个很常见的实际场景。

    举个例子:以购买应用程序为例,用户可能在付费页面进行购买操作,也可能在浏览其他页面时触发购买需求,此时就需要弹出对话框引导用户完成购买行为。

    为了实现这一功能,过去通常会采用以下步骤:

    1. 封装购买组件:首先创建一个通用的购买组件,以便在不同页面和场景下复用。
    2. 在付费页面渲染购买组件:将购买组件直接嵌到付费页面中。
    3. 在其他页面使用 el-dialog 展示购买组件:在其他页面通过 el-dialog 控制组件的显示,利用 visible 状态变量(通常是一个 ref 响应式变量)动态控制对话框的弹出与关闭。

    虽然这种方式可以满足功能需求,但随着该组件被越来越多的页面和功能所使用,维护也会愈加复杂繁琐——每增加一个使用页面,都必须重复编写控制显示/隐藏的逻辑代码。

    那么,有没有更好的方法可以简化这个过程?是否可以通过某种方式,用一个单独的函数全局控制购买组件的打开和关闭,从而减少代码重复,降低维护成本?

    二、关于 useDialog Hook

    在 Vue 中,Hook 允许在函数式组件或者 API 中「钩入」Vue 特性。它们通常在组合式 API(Composition API)中使用,这是 Vue 提供的一套响应式和可复用逻辑功能的集合。

    本文提到的 useDialog Hook 就是一个封装了 el-dialog 组件基本功能的自定义 Hook,它还可以提供附加特性以便在项目中管理和展示弹窗。

    三、实现 useDialog Hook

    useDialog Hook 需要达成以下目标:

    1. 满足基础用法,传入 el-dialog 的基础属性以及默认slot显示的内容,导出 openDialogcloseDialog 函数;
    2. 支持 el-dialog 的事件配置;
    3. 支持默认 slot 组件的属性配置;
    4. 支持 el-dialog 其他 slot 配置,例如 headerfooter 等;
    5. 在内容组件中抛出特定事件支持关闭 dialog;
    6. 支持显示内容为 jsx普通文本Vue Component
    7. 支持在显示内容中控制是否可以关闭的回调函数,例如 beforeClose
    8. 支持显示之前钩子,例如 onBeforeOpen
    9. 支持定义和弹出时修改配置属性;
    10. 支持继承 root vue 的 prototype,可以使用如 vue-i18n$t 函数;
    11. 支持 ts 参数提示;

    (一)准备 useDialog.ts 文件实现类型定义

    import type { Ref } from 'vue'
    import { h, render } from 'vue'
    import { ElDialog } from 'element-plus'
    import type {
      ComponentInternalInstance,
    } from '@vue/runtime-core'
    
    type Content = Parameters<typeof h>[0] | string | JSX.Element
    // 使用 InstanceType 获取 ElDialog 组件实例的类型
    type ElDialogInstance = InstanceType<typeof ElDialog>
    
    // 从组件实例中提取 Props 类型
    type DialogProps = ElDialogInstance['$props'] & {
    }
    interface ElDialogSlots {
      header?: (...args: any[]) => Content
      footer?: (...args: any[]) => Content
    }
    interface Options<P> {
      dialogProps?: DialogProps
      dialogSlots?: ElDialogSlots
      contentProps?: P
    }

    (二)实现普通 useDialog 函数

    下面的函数实现了含目标 1、2、3、4、6 和 11 在内的基础用法。

    目标 1:满足基础用法,传入 el-dialog 基础属性及默认 slot 显示的内容,导出 openDialogcloseDialog 函数;
    目标 2:支持 el-dialog 的事件配置;
    目标 3.:支持默认 slot 组件的属性配置;
    目标 4:支持 el-dialog 其他 slot 配置,如 headerfooter 等;
    目标 6:支持显示内容为 jsx普通文本Vue Component
    目标 11:支持 ts 参数提示;

    export function useDialog<P = any>(content: Content, options?: Ref<Options<P>> | Options<P>) {
      let dialogInstance: ComponentInternalInstance | null = null
      let fragment: Element | null = null
    
      // 关闭并卸载组件
      const closeAfter = () => {
        if (fragment) {
          render(null, fragment as unknown as Element) // 卸载组件
          fragment.textContent = '' // 清空文档片段
          fragment = null
        }
        dialogInstance = null
      }
      function closeDialog() {
        if (dialogInstance)
          dialogInstance.props.modelValue = false
      }
    
      // 创建并挂载组件
      function openDialog() {
        if (dialogInstance) {
          closeDialog()
          closeAfter()
        }
    
        const { dialogProps, contentProps } = options
        fragment = document.createDocumentFragment() as unknown as Element
    
        const vNode = h(ElDialog, {
          ...dialogProps,
          modelValue: true,
          onClosed: () => {
            dialogProps?.onClosed?.()
            closeAfter()
          },
        }, {
          default: () => [typeof content === 'string'
            ? content
            : h(content as any, {
              ...contentProps,
            })],
          ...options.dialogSlots,
        })
        render(vNode, fragment)
        dialogInstance = vNode.component
        document.body.appendChild(fragment)
      }
    
      onUnmounted(() => {
        closeDialog()
      })
    
      return { openDialog, closeDialog }
    }

    (三)实现目标 5

    目标 5:在内容组件中抛出特定事件支持关闭 dialog;

    1. 在定义中支持 closeEventName ;
    interface Options<P> {
      // ...
      closeEventName?: string // 新增的属性
    }
    1. 修改 useDialog 函数接收 closeEventName 事件关闭 dialog。
    export function useDialog<P = any>(content: Content, options?: Ref<Options<P>> | Options<P>) {
      // ...
      // 创建并挂载组件
      function openDialog() {
        // ...
        fragment = document.createDocumentFragment() as unknown as Element
        // 转换closeEventName事件
        const closeEventName = `on${upperFirst(_options?.closeEventName || 'closeDialog')}`
    
        const vNode = h(ElDialog, {
          // ...
        }, {
          default: () => [typeof content === 'string'
            ? content
            : h(content as any, {
              ...contentProps,
              [closeEventName]: closeDialog, // 监听自定义关闭事件,并执行关闭
            })],
          ...options.dialogSlots,
        })
        render(vNode, fragment)
        dialogInstance = vNode.component
        document.body.appendChild(fragment)
      }
    
      onUnmounted(() => {
        closeDialog()
      })
    
      return { openDialog, closeDialog }
    }

    (四)实现目标 7、8

    目标 7:支持在显示内容中控制是否可以关闭的回调函数,例如 beforeClose
    目标 8:支持显示之前钩子,例如 onBeforeOpen

    1. 在定义中支持 onBeforeOpenbeforeCloseDialog 默认传给内容组件,有组件调用设置;
    type DialogProps = ElDialogInstance['$props'] & {
      onBeforeOpen?: () => boolean | void
    }
    1. 修改 useDialog 函数接收 onBeforeOpen 事件并传递 beforeCloseDialog
    export function useDialog<P = any>(content: Content, options?: Ref<Options<P>> | Options<P>) {
      // ...
      // 创建并挂载组件
      function openDialog() {
        // ...
        const { dialogProps, contentProps } = options
        // 调用before钩子,如果为false则不打开
        if (dialogProps?.onBeforeOpen?.() === false) {
          return
        }
        // ...
        // 定义当前块关闭前钩子变量
        let onBeforeClose: (() => Promise<boolean | void> | boolean | void) | null
    
        const vNode = h(ElDialog, {
          // ...
          beforeClose: async (done) => {
            // 配置`el-dialog`的关闭回调钩子函数
            const result = await onBeforeClose?.()
            if (result === false) {
              return
            }
            done()
          },
          onClosed: () => {
            dialogProps?.onClosed?.()
            closeAfter()
            // 关闭后回收当前变量
            onBeforeClose = null
          },
        }, {
          default: () => [typeof content === 'string'
            ? content
            : h(content as any, {
              // ...
              beforeCloseDialog: (fn: (() => boolean | void)) => {
                // 把`beforeCloseDialog`传递给`content`,当组件内部使用`props.beforeCloseDialog(fn)`时,会把fn传递给`onBeforeClose`
                onBeforeClose = fn
              },
            })],
          ...options.dialogSlots,
        })
        render(vNode, fragment)
        dialogInstance = vNode.component
        document.body.appendChild(fragment)
      }
    
      onUnmounted(() => {
        closeDialog()
      })
    
      return { openDialog, closeDialog }
    }

    (五)实现目标 9、10

    目标 9:支持定义和弹出时修改配置属性;
    目标 10:支持继承 root vue 的 prototype,可以使用例如 vue-i18n$t 函数;

    // 定义工具函数,获取计算属性的option
    function getOptions<P>(options?: Ref<Options<P>> | Options<P>) {
      if (!options)
        return {}
      return isRef(options) ? options.value : options
    }
    
    export function useDialog<P = any>(content: Content, options?: Ref<Options<P>> | Options<P>) {
      // ...
      // 获取当前组件实例,用于设置当前dialog的上下文,继承prototype
      const instance = getCurrentInstance()
      // 创建并挂载组件,新增`modifyOptions`参数
      function openDialog(modifyOptions?: Partial<Options<P>>) {
        // ...
        const _options = getOptions(options)
        // 如果有修改,则合并options。替换之前的options变量为 _options
        if (modifyOptions)
          merge(_options, modifyOptions)
    
        // ...
    
        const vNode = h(ElDialog, {
          // ...
        }, {
          // ...
        })
        // 设置当前的上下文为使用者的上下文
        vNode.appContext = instance?.appContext || null
        render(vNode, fragment)
        dialogInstance = vNode.component
        document.body.appendChild(fragment)
      }
    
      onUnmounted(() => {
        closeDialog()
      })
    
      return { openDialog, closeDialog }
    }

    通过上面的封装使用 useDialog Hook 后,需要弹窗时,只需要引入该 Hook 并调用 openDialog 方法,非常方便简洁。此外,这样的封装也会让后续修改弹窗逻辑变得更加方便,只需要在 useDialog Hook 中修改,无需逐个重复编辑。

    四、useDialog Hook 案例实操

    下面,我们使用 useDialog Hook 来解决开头提到的应用程序购买问题。

    (一)创建 components/buy.vue 购买组件

    <script lang="ts" setup>
      const props = defineProps({
        from: {
          type: String,
          default: '',
        },
      })
    </script>
    <template>
      我是购买组件
    </template>

    (二)在 pages/subscription.vue 页面中使用 buy.vue 购买组件

    <script lang="ts" setup>
      import Buy from '@/components/buy.vue'
    </script>
    <template>
    
      <Buy from="subscription" />
    
    </template>

    (三)在其他功能页面中弹出 buy.vue 购买组

    <script lang="ts" setup>
      import { useDialog } from '@/hooks/useDialog'
      const Buy = defineAsyncComponent(() => import('@/components/buy.vue'))
    
      const { openDialog } = useDialog(Buy, {
        dialogProps: {
          // ...
          title: '购买'
        },
        contentProps: {
          from: 'function',
        },
      })
    
      const onSomeClick = () => {
        openDialog()
      }
    </script>

    拓展:useDialog Hook 的其他应用

    beforeClose & closeEventName 示例:buy.vue 购买组件

    <script lang="ts" setup>
      const props = defineProps({
        from: {
          type: String,
          default: '',
        },
        beforeCloseDialog: {
          type: Function,
          default: () => true,
        },
      })
    
      const emit = defineEmits(['closeDialog'])
    
      props.beforeCloseDialog(() => {
        // 假如from 为 空字符串不能关闭
        if (!props.from) {
          return false
        }
        return true
      })
    
      // 关闭dialog
      const onBuySuccess = () => emit('closeDialog')
    </script>
    <script lang="ts" setup>
      import { useDialog } from '@/hooks/useDialog'
      const Buy = defineAsyncComponent(() => import('@/components/buy.vue'))
    
      const { openDialog } = useDialog(Buy, {
        dialogProps: {
          // ...
          title: '购买'
        },
        contentProps: {
          from: '',
        },
      })
    
      const onSomeClick = () => {
        openDialog()
      }
    </script>

    总结

    使用 useDialog Hook 封装 el-dialog 可以让前端技术更加有趣简洁。笔者也希望大家能尝试这样的封装方式,让前端代码更加优雅且易于维护。

    优秀的工程师就同优秀的厨师一样,掌握了精妙的烹饪和调味技巧,就能让每道菜都变得美味可口!


    >> LigaAI 往期精彩阅读 <<

    LigaAI x 极狐GitLab,共探 AI 时代研发提效新范式

    宁波银行:在「金融科技」引擎上,沉浸式提效减负

    生成式 AI 如何释放开发者的生产力?

    新晋技术管理者如何推动组织变革?

    欢迎试用 LigaAI-智能研发协作平台,体验智能研发协作,一起变大变强!

    ]]>
    精彩回顾 | 「AI 驱动增长,研发数智化升级」分享沙龙成功举办 https://ligai.cn/blog/team/1492.html Thu, 11 Apr 2024 10:48:21 +0000 https://ligai.cn/blog/?p=1492 阅读更多]]> AI 应用元年,人工智能技术将如何助力企业发展新质生产力,构建增长动能?

    日前,LigaAI 与深圳市企业联合会、西云数据联合举办了「AI 驱动增长,研发数智化升级」技术专题沙龙。本次活动围绕「AI+」应用实践,邀请到 LigaAI 联合创始人兼 CTO 张思、西云数据高级解决方案架构师王高雄分享生成式 AI 在具体工作及业务场景中的落地经验;到场嘉宾们还就「智能化手段对研发效能的提升及对企业增长的影响」展开了交流与探讨。

    活动特别邀请到深圳市企业联合会康永魁会长出席并致辞。康会长指出,AI 命题紧扣大湾区发展和高质生产力的时代要求,企业探索 AI 驱动力,积极推动新技术在企业研发、管理及业务等领域的应用,将有助于思路启发、视野开拓、技术赋能和管理创新。

    活动中,LigaAI 联合创始人兼 CTO 张思作了题为「AI 当道,研发如何驱动企业越阶增长」的分享。他从 AI 应用场景选择到 AI 提效的量化与管理,全面解读了生成式 AI 可以如何为产研团队所用并持续赋能——借助「三环模型」筛选可 AI 化的具体场景,通过 LigaAI 「采集-跟踪-诊断-提升」四步曲量化和优化 AI 效益。

    张思强调,AI 技术在工作场景中全方位融合已成为必然趋势,对 AI 祛魅才能更好地让新技术为己所用:Copilot、Auto-agent 和 AI Agent 都是数字化劳动力,其本质都是企业的资产和工具,无需过度「神化」。

    西云数据高级解决方案架构师王高雄则带来「生成式 AI 助力企业出海,赋能业务扩张」的精彩分享。他首先介绍了生成式 AI 的最新成果和行业动态,又深入浅出地讲解了 Amazon Bedrock 在助力企业出海方面的场景应用和案例经验。

    他指出,生成式 AI 是集高科技、高质量与高效能于一身的重要的新质生产力,而 Amazon Bedrock 已在检索增强(RAG)、虚拟角色塑造、文本分析/生成、图片生成与重塑等业务场景提供支持。

    主题分享结束,思维的激烈碰撞正式拉开帷幕。现场嘉宾们围绕行业落地和业务结合经验展开交流和分享,更深入探讨了 AI 及数字劳动力对组织架构、流程优化、人效提升和研发效能等的影响。

    活动总结

    人工智能无疑是当今时代最具变革性的技术力量,也正在以惊人的速度重塑各行各业的生产协作方式。作为一家以 AI 技术为核心的企业服务公司,LigaAI 相信 AI 技术将为企业带来更多增长机遇和创新动力。

    期待下一次相遇,期待与更多朋友一起见证 AI 带来的行业飞跃。

    关于 LigaAI

    LigaAI 是新一代智能研发协作平台。我们以人工智能技术为核心,致力于通过 AI 场景化繁为简,提升协作效率,赋能广大研发团队。从开发者的具体工作场景出发,LigaAI 通过人工智能将开发者们从繁杂琐事中抽离出来,为其提供简洁、智能的协作体验,也为不同类型的组织提供数字化、个性化、智能化的项目协作平台。


    >> LigaAI 往期精彩阅读 <<

    宁波银行:在「金融科技」引擎上,沉浸式提效减负

    生成式 AI 如何释放开发者的生产力?

    新晋技术管理者如何推动组织变革?

    欢迎试用 LigaAI-智能研发协作平台,体验智能研发协作,一起变大变强!

    ]]>
    LigaAI 的 8 个年度关键词 | 2023 年度盘点 https://ligai.cn/blog/team/1351.html Sun, 04 Feb 2024 02:30:53 +0000 https://ligai.cn/blog/?p=1351 阅读更多]]> 如果说 2021-2022,LigaAI 是知无不尽的「敏捷布道师」,那么 2023,LigaAI 则是力学笃行的「实践共享家」。

    过去一年,颠覆性的创新技术和成果频频涌现,外部环境不确定性加剧。在那些或抓马或平凡的日子里,LigaAI 持续分享了许多优质的最佳实践与成功经验,试图为广大研发团队带去一些确定性。

    随着 2023 正式汇入时间长河,LigaAI 也整理了这一年中备受好评的重磅文章,并将一整年的创意、想法和思考精炼为 8 个年度关键词,希望与你一起回顾新一年的收获与成长。

    01AI 赋能

    2023 年,是 AGI 和大模型空前发展的一年:ChatGPT 点燃全行业对大模型的探索热情,从「百模大战」到应用落地,全世界都为之疯狂。作为一个以 AI 技术为核心且高度重视效率的团队,LigaAI 一直在挖掘「AI 赋能研发提效」的无限可能;在不断强化产品力的同时,我们也听到许多焦虑和质疑的声音:

    • 当 AI 可以听、说、读、写、看、画,甚至思考,「我」会不会被取代?
    • 类似 ChatGPT 的能力能多大程度地帮助组织提升工作效率?
    • 当「AI 布局」成为角逐新高地,生成式 AI 会如何影响产品形态,重塑产业价值?

    大模型元年,LigaAI 在反复追问、思考、实践和验证中去虚务实,更加坚定了「用 AI 技术赋能广大研发团队」的创业初心。

    👍👍👍

    《ChatGPT后,B端产品设计会迎来颠覆式革命吗?》

    《对话 ChatGPT:现象级 AI 应用,将如何阐释「研发效能管理」?》

    《五一特辑 | 人类 vs AI:玩梗大作战,看看谁是最后的赢家?》

    02:研发效能度量

    数字化时代,提升研发效能,加快高质量价值交付已然是众多企业的核心竞争力和重要目标之一。有度量才有管理——研发管理提效自然也离不开精准可靠的量化指标。

    LigaAI 从速度、质量和价值等维度出发,整理并解析了含 NPS、Cycle Time、Lead Time、MTTR 等在内的常见效能度量指标;通过提供一个具象直观的数据管理视图,帮助研发管理者更科学、更全面地洞察组织交付能力,精细化提效增长。

    👍👍👍

    《什么是研发 Lead Time?》

    《用Cycle Time做度量,优化研发交付效率成功一半》

    《9 个研发质量管理指标,一次理清!》

    《质量指标大PK:MTTR vs MTBF,谁是靠谱王?》

    03可持续生产力

    毫无疑问,世界正在阔步迈向 AI 驱动的时代。当开发复杂度和研发管理复杂度日渐攀升,「如何让开发工作效率以同等速率跟上增长」或将成为围困组织的灵魂拷问。秉承着「以人为本」的价值观,LigaAI 期望助力企业建立一支健康稳定、敏捷高效、自组织的研发强军,赋能企业维护可持续的研发生产力。

    高绩效团队通常具备哪些优势和特点?研发管理者可以如何科学管理及提升组织生产力,并长期维护健康的可持续工作状态?欲知详情如何,且看下列分享😉~

    👍👍👍

    《高绩效团队的 5 个优秀习惯,看看你占了几个?》

    《如何科学判断研发团队是否在健康工作?》

    《准AI时代,如何衡量程序员的工作效率和生产力?》

    04:业务提效

    生成式 AI 有望带来相当于全球行业收入 9% 的新增价值,而工作与协作范式的转变也为突破增长困局带来转机。行业新贵频出,企业换代速度加快让「理解业务,稳定增长」成为企业生存的第一要义,也给研发团队提出了更高的要求。

    作为离前沿技术最近的部门,研发团队必须为「如何充分释放 AI 和工具的潜力并为企业创造业务价值」递上一份令人满意的答卷。关于软件工具和 AI 能力的驾驭技巧,LigaAI 有话要说🙋

    👍👍👍

    《宁波银行:在金融科技引擎上,沉浸式提效减负》

    《论 PMF 和结果导向如何影响组织效能?》

    《生成式 AI 如何释放开发者的生产力?》

    05技术管理进阶

    “要做企业和研发团队的陪跑者”,LigaAI CEO 周然如是说。任何企业和组织都离不开管理者,而在「技术晋升管理」相当普遍的研发领域,两眼一抹黑的无所适从却才是新晋管理者的工作常态。

    围绕「技术管理进阶」,LigaAI 创作和分享了一系列干货文章,全面拆解了从技术到管理、从「管事」到「管人」的常见难题及解决办法,以帮助更多(准)技术管理者更快、更好地开展新工作。

    👍👍👍

    《压箱底的技术管理成长经验分享,请收好!》

    《新晋技术管理者如何推动组织变革?》

    《向上管理:三个技巧,教你如何与老板高效协作》

    06:技术狂欢

    将工程师文化贯彻到底。这一年,LigaAI 持续分享了多篇技术解决方案,并有幸在多个开发者社区获得程序员朋友们的广泛认可。在坚持做知识沉淀和技术交流的日子里,「看见自我成长」也荣登我司开发者们的「成就感榜单」榜首。

    稳住心态做研发,静下心来搞技术。未来,LigaAI 还将继续以「为开发者社区添砖加瓦」为己任,持续分享技术博客,坚定务实地向上生长。

    👍👍👍

    《72小时灵感冲刺,创意就该这么玩 | Hackathon特别策划》

    《前端进阶:如何在 Web 中使用 C++?》

    《如何编写同时兼容 Vue2 和 Vue3 的代码?》

    《ChatGPT API 调用总超时?解决办法在这》

    07SaaS 出海

    2023,中国企业出海火热依旧。与往年 ToC 产品引领出海不同,这一年越来越多 ToB SaaS 企业主动地在国际化市场寻找第二增长曲线。

    出海掘金潮下,LigaAI 不由地好奇国内外市场需求是否「有壁」?一个主要由中国人组成的研发团队如何提升组织能力,才能快速响应和服务国际客户的需求?为此,LigaAI 专门向拥有国际化基因的出海领先企业讨教了一番。

    👍👍👍

    《SaaS 出海,如何搭建国际化服务体系?》

    08:价值驱动

    价值,是过去一年在我司出现频率最高的词汇,没有之一。三年多来, LigaAI 回答得最多的问题是「和其他研发管理工具相比,你们有什么不同?」ChatGPT 带着 AI 飓风席卷全球后,LigaAI(一家「标榜」以 AI 技术为核心的公司)难免被频繁地问到「你们的 AI 能做什么?」

    三周年那天,我们正式向大家告白「LigaAI 为什么而存在?」——颠覆低效的研发协作模式、消除顽固的研发管理盲区、建立起以科学度量和精细化管理为基座的价值交付舰艇,智能化赋能研发管理。

    👍👍👍

    《LigaAI:从效率、度量和价值维度,成为研发团队的智能医生》

    写在最后

    2023,变化依旧是时代主旋律。经历增长放缓、「AI 入侵危机」和「出海淘金记」等时代巨制后,LigaAI 反而能更加专注且坚定地关注自我跃升,向内生长、向外突破。

    这一年,我们更坚定要用 AI 技术赋能研发团队;

    下一年,我们还将继续以 AI 技术赋能企业成功。

    2024 赋能研发提效,LigaAI 步履不停!


    欢迎试用 LigaAI-智能研发协作平台,体验智能研发协作,一起变大变强!

    ]]>
    宁波银行:在「金融科技」引擎上,沉浸式提效减负 https://ligai.cn/blog/team/1336.html Mon, 25 Dec 2023 09:01:13 +0000 https://ligai.cn/blog/?p=1336 阅读更多]]> “流程规范做加法,效率优化做减法。”宁波银行研发平台负责人徐老师同 LigaAI 分享道,“我们希望能以一种更安全轻巧的方式,提升研发效能。”

    将金融科技作为重要生产力,宁波银行坚持深化科技与业务融合,是金融行业数字化转型的典范。在研发流程数字化建设早期,宁波银行率先实现从代码提交到发版上线的一体化升级。伴随业务水平和企业目标的发展,「研发全流程数智化管理」被提上日程,而为研发协作而生的 LigaAI IDE 插件正是打通需求到开发,搭建价值流「最后一公里」的重要一环

    围绕沉浸式、自动化和精细化三个目标,宁波银行与 LigaAI 基于「奋进号」平台,联手打造「LigaAI IDE 提升项目」。 通过减少个人时间浪费、抹平团队协作障碍,为行内开发者打造沉浸式工作体验,以数据驱动赋能研发管理。

    01 LigaAI IDE 插件:沉浸式工作,小改善有大跃升

    走在行业数智化建设前列,宁波银行也面临着研发流程信息化的伴生问题:项目信息和开发工作分散在不同工具中,研发人员需要在多个系统和平台间反复跳转才能查看需求、管理和跟踪研发工作。 开发者的工作节奏被迫打乱,无法专注思考和编码,而依赖手工更新状态既无法保证时效,还会在上传下达中浪费研发资源。

    LigaAI IDE 提升项目无缝连接需求管理与代码管理,覆盖 IDEA 、Android Studio、VS Code 和 Eclipse 等宁波银行所需的集成开发环境(IDE),以插件形式为研发人员打造沉浸式个人工作空间。得益于 LigaAI 为其特别定制的工作类型、详情字段和关联管理等,开发者们无需切换至奋进号或 IM 工具,在 IDE 内就能直接查看个人待办工作、了解完整的需求详情、掌握项目进展、更新和传递需求状态、提交反馈和评论,高效管理每日工作

    LigaAI IDE 插件实时同步项目更新与变更,不仅增强了高价值信息的流通性,还让开发者免受工具切换、思路阻断的困扰,最大程度保障了充足的专注时间,释放更多生产力。

    LigaAI IDE 插件 | 一站式管理研发工作

    改善工作体验少不了「沉浸式代码评审」。基于奋进号的开放能力,LigaAI IDE 插件将代码托管平台的能力嵌入 IDE,不仅支持 MR 的快速创建和自动关联,还支持记录和查看代码修改详情,通过将代码差异与详情以清晰直观的方式统一起来,帮助开发者高效完成代码评审,提升整体工作效率。

    02 LigaAI IDE 插件:自动化提效,攻克协作信息差

    安全、合规和风控是金融科技发展和创新的底线,对宁波银行而言,研发流程规范和审核制度再怎么严谨都不为过,但这难免会给力求高效和流畅的研发协作带来「小烦恼」。

    代码管理为例,宁波银行在探索实践与持续改进中,对创建/合并分支、提交代码、代码评审等环节提出了明确的操作要求和规范,还设置了多个如代码关联核验的检查节点和质量管理门禁。

    过去,开发者们需要自行根据工作类型及分支命名规范,手动为每张卡片创建和拉取分支。 如果遇到突发的缺陷或事务,即便能够快速编码,也常常会因为卡片信息缺失或分支命名错漏而流转受阻。

    现在,有了 LigaAI IDE 插件将整个过程智能化,开发者可以在 IDE 内快速创建卡片和分支,自动关联开发任务或事务,并自动填充需求信息。 在简化操作路径的同时,LigaAI IDE 插件用强大的自动化功能代替手工创建及填报,提高研发进程中信息的准确性和及时性,为高效协作打下基础。

    LigaAI IDE 插件 | 及时流转才能高效协作

    提到代码关联管理,研发平台负责人彭老师告诉 LigaAI,行内开发者提交代码时,如果未按要求提供卡片、人员等信息或者提交了错误的信息,都会导致 PR 失败;同时,受到卡片变更规则和质量管理门禁的约束,回退代码也不容易。

    因此,研发人员通常会从提前整理好的文档或者已提交的代码中复制模板,再手动替换内容。即便如此,信息错漏导致的提交失败和返工依旧时有发生。

    从开发者的具体工作场景出发,LigaAI IDE 插件「智能 Commit Message」功能很好地解决了这个问题。借助 LigaAI 模板化和自动化的能力,开发者能轻松实现代码与卡片的精准关联,有效改善因信息错报、漏报导致的返工和浪费,为后续顺畅的研发协作提供保障。

    LigaAI IDE 插件 | 智能合并请求,告别手工提交

    03 LigaAI IDE 插件:精细化管理,击破效能瓶颈

    践行数智化管理,研发效能度量体系和可视化分析是强有力的臂膀,宁波银行深谙此道。由于行内业务繁多、开发者们使用的 IDE 种类不一,且 IDE 原生能力无法满足组织对数据和权限的复杂要求,宁波银行曾受限于缺失的开发数据和时效滞后的进度信息,难以有效开展有据可查的管理赋能。

    引入 LigaAI IDE 提升项目后,宁波银行在奋进号原有的 DevOps 能力基础上,补全了「需求管理 – 代码开发」的价值链路,全面打通端到端数据采集通道,真正实现研发全流程数据留痕。

    利用真实完整且实时同步的团队过程数据,管理者可以构建可靠的数据分析闭环,并根据目标与愿景确定度量指标;结合「可视化效能视图」和「数据洞察面板」,还能快速洞悉关键指标的变化趋势,甄别异常与潜在风险,及时制定针对性优化策略,精准打击效能瓶颈,提升研发效能。

    LigaAI IDE 插件 | 数据驱动的精细化管理

    LigaAI IDE 插件优秀的一手数据采集能力同样备受组织流程规范的青睐。 以制度性的「工时登记」为例,开发者通过插件内嵌的「报工」功能,在编码完成后即可填报/补报实际工时和工作内容,无需担心忘报、漏报。管理者基于及时同步的一线反馈,也能更合理地分配团队资源,提高组织的风险管控能力。

    写在最后

    数字经济时代,金融数智化转型已成为广泛共识。积极发展金融科技,加快推进数智化转型是金融行业的时代快车。

    在 LigaAI 的助力下,宁波银行通过数智化建设探索「如何将有限的研发资源投入在更高价值的需求上」——以人为本,沉浸式体验释放生产力;化繁为简,自动化刷新协作效率;数据驱动,精细化赋能效能提升。

    在提质增效的浪潮中,宁波银行将持续加大金融科技资源投入,全面推进数智化金融服务改造,加速系统化、数字化、智能化布局落成,持续优化客户服务体验,更好地赢得客户、服务客户、经营客户。

    未来,LigaAI 也将与宁波银行一起,持续探索更多数字化、自动化和智能化落地空间,帮助其抢抓金融数智化转型机遇,全面为企业提效减负。

    减少个人时间浪费,抹平协作障碍,赋能企业提质增效,欢迎使用 LigaAI 智能研发协作平台。

    ]]>
    技术分享 | ChatGPT API 调用总超时?解决方案在这 https://ligai.cn/blog/sharing/1334.html Wed, 13 Dec 2023 04:50:57 +0000 https://ligai.cn/blog/?p=1334 阅读更多]]> 问题重现

    在调用 ChatGPT API 并使用流式输出时,我们经常会遇到网络问题导致的超时情况。有趣的是,笔者发现在本地调试遇到的超时,会在 10 分钟后自动恢复(为什么是 10 分钟?我们留到后面解释),但是在服务器上等待一会儿却会失败,报出超时异常(错误代码 502)。

    笔者认为,本地能恢复的原因可能是自动重试,只是重试的时间有点久(ChatGPT API 没有重试功能,这是项目加入的)。服务器返回「502」是因为内容从后台返回到前端需要经过网关层,而网关层超时校验的时间比自动重试的时间(10 分钟)更短,所以撑不到重试就会报超时异常。

    基于以上场景,本文着手解决 ChatGPT API 调用超时问题。

    优化诉求

    • 不向用户展示超时的报错信息。
    • 缩短超时后重试的时间间隔。

    解决思路

    笔者考虑了两种方案。

    一是彻底解决网络问题,但难度有点大。 这属于 OpenAI 服务器问题,即使是部署在国外的服务器也会出现超时的情况。

    二是利用自动重试解决问题。 通过调整超时的时间,提升响应速度,方案可行。

    实施解决方案

    解决过程中,笔者分两步由浅至深地调整了超时时间;如果想直接了解最终方案,请移步「解决方案二」~

    • 运行环境:

    Python: 3.10.7

    openai: 0.27.6

    • 调用方法:

    openai.api_resources.chat_completion.ChatCompletion.acreate

    ( 这是异步调用 ChatGPT 的方法。)

    • 方法调用链路:

    超时参数 ClientTimeout,一共有 4 个属性 totalconnectsock_readsock_connect

    # 方法 -> 超时相关参数
    openai.api_resources.chat_completion.ChatCompletion.acreate -> kwargs
    openai.api_resources.abstract.engine_api_resource.EngineAPIResource.acreate -> params
    openai.api_requestor.APIRequestor.arequest -> request_timeout
    # request_timeout 在这一步变成了 timeout,因此,只需要传参 request_timeout 即可
    openai.api_requestor.APIRequestor.arequest_raw -> request_timeout
    aiohttp.client.ClientSession.request -> kwargs
    aiohttp.client.ClientSession._request -> timeout
        tm = TimeoutHandle(self._loop, real_timeout.total) -> ClientTimeout.total
        async with ceil_timeout(real_timeout.connect): -> ClientTimeout.connect
    # 子分支1
    aiohttp.connector.BaseConnector.connect -> timeout
    aiohttp.connector.TCPConnector._create_connection -> timeout
    aiohttp.connector.TCPConnector._create_direct_connection -> timeout
    aiohttp.connector.TCPConnector._wrap_create_connection -> timeout
        async with ceil_timeout(timeout.sock_connect): -> ClientTimeout.sock_connect
    # 子分支2
    aiohttp.client_reqrep.ClientRequest.send -> timeout
    aiohttp.client_proto.ResponseHandler.set_response_params -> read_timeout
    aiohttp.client_proto.ResponseHandler._reschedule_timeout -> self._read_timeout
        if timeout:
        self._read_timeout_handle = self._loop.call_later(
            timeout, self._on_read_timeout
        ) -> ClientTimeout.sock_read

    解决方案一

    openai.api_requestor.APIRequestor.arequest_raw 方法中的 request_timeout 参数可以传递 connecttotal 参数.

    因此可以在调用 openai.api_resources.chat_completion.ChatCompletion.acreate时,设置 request_time(10, 300)

    #
    async def arequest_raw(
        self,
        method,
        url,
        session,
        *,
        params=None,
        supplied_headers: Optional[Dict[str, str]] = None,
        files=None,
        request_id: Optional[str] = None,
        request_timeout: Optional[Union[float, Tuple[float, float]]] = None,
    ) -> aiohttp.ClientResponse:
        abs_url, headers, data = self._prepare_request_raw(
            url, supplied_headers, method, params, files, request_id
        )
    
        if isinstance(request_timeout, tuple):
            timeout = aiohttp.ClientTimeout(
                connect=request_timeout[0],
                total=request_timeout[1],
            )else:
                timeout = aiohttp.ClientTimeout(
                    total=request_timeout if request_timeout else TIMEOUT_SECS
                )
        ...

    该方案有效,但没有完全生效:它可以控制连接时间和请求的全部时间,但没有彻底解决超时异常,因为「请求连接时间」和「第一个字符读取时间」是两码事。「请求连接时间」基于 total 时间重试(300s),而网关时间并没有设置这么久。

    于是,笔者继续提出「解决方案二」。

    解决方案二

    使用 monkey_patch 方式重写 openai.api_requestor.APIRequestor.arequest_raw 方法,重点在于重写 request_timeout 参数,让其支持原生的 aiohttp.client.ClientTimeout 参数。

    1. 新建 api_requestor_mp.py 文件,并写入以下代码。

    # 注意 request_timeout 参数已经换了,Optional[Union[float, Tuple[float, float]]] -> Optional[Union[float, tuple]]
    async def arequest_raw(
            self,
            method,
            url,
            session,
            *,
            params=None,
            supplied_headers: Optional[Dict[str, str]] = None,
            files=None,
            request_id: Optional[str] = None,
            request_timeout: Optional[Union[float, tuple]] = None,
    ) -> aiohttp.ClientResponse:
        abs_url, headers, data = self._prepare_request_raw(
            url, supplied_headers, method, params, files, request_id
        )
        # 判断 request_timeout 的类型,按需设置 sock_read 和 sock_connect 属性
        if isinstance(request_timeout, tuple):
            timeout = aiohttp.ClientTimeout(
                connect=request_timeout[0],
                total=request_timeout[1],
                sock_read=None if len(request_timeout) < 3 else request_timeout[2],
                sock_connect=None if len(request_timeout) < 4 else request_timeout[3],
            )
        else:
            timeout = aiohttp.ClientTimeout(
                total=request_timeout if request_timeout else TIMEOUT_SECS
            )
        if files:
            # TODO: Use aiohttp.MultipartWriter to create the multipart form data here.
            # For now we use the private requests method that is known to have worked so far.
            data, content_type = requests.models.RequestEncodingMixin._encode_files(  # type: ignore
                files, data
            )
            headers["Content-Type"] = content_type
        request_kwargs = {
            "method": method,
            "url": abs_url,
            "headers": headers,
            "data": data,
            "proxy": _aiohttp_proxies_arg(openai.proxy),
            "timeout": timeout,
        }
        try:
            result = await session.request(**request_kwargs)
            util.log_info(
                "OpenAI API response",
                path=abs_url,
                response_code=result.status,
                processing_ms=result.headers.get("OpenAI-Processing-Ms"),
                request_id=result.headers.get("X-Request-Id"),
            )
            # Don't read the whole stream for debug logging unless necessary.
            if openai.log == "debug":
                util.log_debug(
                    "API response body", body=result.content, headers=result.headers
                )
                return result
            except (aiohttp.ServerTimeoutError, asyncio.TimeoutError) as e:
                raise error.Timeout("Request timed out") from e
            except aiohttp.ClientError as e:
                raise error.APIConnectionError("Error communicating with OpenAI") from e
    
    def monkey_patch():
        APIRequestor.arequest_raw = arequest_raw

    2. 在初始化 ChatGPT API 的文件头部补充:

    from *.*.api_requestor_mp import monkey_patch
    
    do_api_requestor = monkey_patch

    设置参数 request_timeout=(10, 300, 15, 10) 后,再调试就没什么问题了。

    交付测试,通过。

    经验总结

    • 直接看代码、看方法调用链路会有点困难,可以通过异常堆栈来找调用链路,这样更方便。
    • ChatGPT API 暴露的 request_timeout 参数不够用,需要重写;搜索了一下重写方案,了解到 monkey_patch,非常实用。
    • 项目过程中,笔者发现改代码本身不难,难的是知道「改哪里」「怎么改」以及「为什么」。

    >> LigaAI 往期精彩阅读 <<

    在 IDE 插件开发中接入 JCEF 框架 – LigaAI 团队博客 (ligai.cn)

    准「AI 时代」下,如何衡量程序员的工作效率和生产力?

    生成式 AI 如何释放开发者的生产力?

    新晋技术管理者如何推动组织变革?

    欢迎试用 LigaAI-智能研发协作平台,体验智能研发协作,一起变大变强!

    ]]>
    Liga妙谈 | SaaS 出海,如何搭建国际化服务体系? https://ligai.cn/blog/liga%e5%a6%99%e8%b0%88/1312.html Thu, 26 Oct 2023 12:03:33 +0000 https://ligai.cn/blog/?p=1312 阅读更多]]>

    防噎指南:这可能是你看到的干货含量最高的 SaaS 出海经验分享,请就水放肆食用(XD。

    当越来越多中国 SaaS 企业选择开启「国际化」副本,出海便俨然成为国内 SaaS 的新·角斗场。

    LigaAI 观察到,出海浪潮助生了许多「Day One 就做国际化」的产品和团队,而出海大军中也不乏在国内已经完成 PMF 的熟悉面孔。一个主要由中国人构成的团队在推进海外产品落地的过程中,可能会遇到哪些困难?是否能以行之有效的「避雷方案」,加快行军速度,一路披荆斩棘?

    为解答心中的疑惑,#Liga妙谈# 第九期特别邀请 Aftership 全球客户成功副总裁刘徽(Sean)与我们一起探讨中国 SaaS 企业出海的「路与行」。

    刘徽老师在 To B 领域拥有 12 年的资深从业经验,是业界畅销书《客户成功:持续复购和利润陡增的基石》的作者;他曾在销售易、Moka、字节跳动(飞书产品)担任过客户成功负责人,对于目前处于互联网风口的企业服务领域和 SaaS 模式有着深刻理解。

    • 中国 SaaS 企业出海会遇到哪些常见难题?
    • 哪些 SaaS 产品或团队更适合发展出海业务?
    • 大/中/小型企业如何落地国际化服务体系?
    • 异步团队如何开展更高效的跨时区协作?

    以上这些问题,你都能在本文找到可复制、可实践、可落地的答案。

    妙谈嘉宾 | 刘徽

    内容整理 | LigaAI


    01 中国 SaaS 出海的两大难题

    LigaAI:从国内与国际市场差异的角度看,中国 SaaS 企业出海可能会碰到哪些问题?

    刘徽:出海企业可能遇到的主要问题在于「合规性」方面。国外对数据安全和个人隐私的重视程度、相关的处罚力度都远超国内,企业一旦触犯违规红线,很可能会背负巨额罚单,所以在投放市场之前,一定要先深度了解当地相关的政策和法律法规,不能照搬「先成长,后合规」的老套路。

    涉及数据安全与隐私问题,免不了要考虑基础设施建设。出海企业的云服务器、服务中心和数据中心的布局和选型也要符合当地的相关要求。

    另外,出海企业应申请一些权威认证,如 ISO、SOC 2、GDPR 等,以展示其合规性与安全性。

    除了合规,海外也很重视客户评价。国外有专门的用户测评网站,如 G2、Trustpilot;出海企业应重视并积极获取真实的客户评价,切忌「刷好评」

    LigaAI:SaaS 企业如何在出海早期,获得优质的、高质量的真实评价,将口碑积累起来?

    刘徽:我的经验是,与尽可能多的客户互动(而不是只聚焦一小波客户),帮助他们解决问题,再顺势提出留评请求。

    比如,我们有专门的团队为免费用户提供服务和帮助。服务人员会在对话结束前,给对方发送一个链接并询问能否为我们写一个评论;同时,团队还会跟踪 Chat-to-Review 的转化率,即每天进线的对话中有多少用户最终在其他用户评价网站上成功留评。这样虽然牺牲了成本和效率,但也最大程度地增加了我们与客户互动的机会,进而可以争取到更多评价。

    产品设计方面,可以通过「权益升级」的方式,引导用户主动联系我们。举个例子,设计一些需要找服务团队开通/升级的小功能点,在发放权益后(即成功解决了问题),再向客户发起留评请求。

    而客户成功(CSM)会在重要客户 Onboard 结束,且能较为顺利地使用产品时,邀请对方前往 G2 等网站留下真实评价。

    另外,邀评有一个非常严格的要求:绝对不能强制客户留好评。一方面,海外客户非常看重「诚信」,强制/诱导对方留好评很可能引起反感而适得其反;另一方面,邀评的主要目的是获得更多真实的客户评价,提高数量,而不是刷评分。所以在话术上要避免「五星好评」「Positive」等说法。

    02 没有不适合出海的 SaaS 企业

    LigaAI:以你对 SaaS 行业的了解和观察,你认为所有 B 端 SaaS 企业都适合出海吗?

    刘徽:我倒没有觉得哪种产品不适合出海。坦白来讲,我还没有接触过从中国诞生,再延展到海外的 SaaS 产品;大部分企业还是海外独角兽的「跟随者」。它们结合了创新和延伸后,才向海外拓展。

    不过,从产品功能的角度来看,海外确实有一些特殊性。比如说,国内适用的「消息已读/未读」「发红包」等功能就不适合腾挪到海外产品上——这是文化差异影响的。所以,即便产品本身适合出海,也要结合当地的文化和习惯,在功能上做出相应的调整或改变。

    LigaAI:拥有哪些基因的团队更适合做出海生意?

    刘徽:我个人认为拥有海外视野的团队——比方说,公司创始人、产品负责人或者核心骨干有过海外留学或工作的经历,长时间在海外生活过——在处理出海业务上,会更有优势。

    对 To B 产品设计而言,了解目标客户的工作方式和工作习惯很重要。如果团队所有的经历都发生在国内,仅凭客户访谈很难深入了解海外的文化和工作方式。

    LigaAI:假如创始人和核心团队都没有海外背景的加持,团队还可以通过哪些方式了解当地的文化和工作习惯?

    刘徽:说实话,这种情况还挺难的。建议至少要招 1-2 个有海外工作和生活经验的人。如果实在招不到,那就用比较传统的方式,比如客户访谈、海外竞品分析等了解、学习和模仿产品设计思路。

    国内比较成熟的或者快要成型的产品直接出海的难度和风险都会比较高。因为此时根据市场反馈调整产品更为困难,所以最好是在产品雏形阶段就到海外去试水和验证

    还有一种可能是像飞书和 Lark 一样,一开始就计划好国内外产品走两套体系,而有海外逻辑的就往出海发展。

    LigaAI:中小企业很可能没办法做到「两条腿走路」。年轻一些的创业团队可能从 Day One 开始就明确要做全球化、国际化的业务,所以产品从一开始就按照海外的逻辑去设计,团队组建也尽可能寻找有海外背景、海外服务经验的人。

    但是,对于在国内已经有成熟产品的中小企业而言,如果想要开拓海外市场,是不是建议要单独组建一支队伍专门负责海外产品的规划,比如从产品到服务一体化的落地,甚至整个产品形态的重塑?

    刘徽:是的。如果由原来的国内团队继续往海外方向做,很可能会出现思维固化、认知局限等问题,进而很难产生新的思维突破。所以,我更建议单拉一个团队,以全新的视野专注地做好出海业务。

    03 国际化服务体系搭建的两种模式

    LigaAI:出海的 SaaS 企业如何判断搭建和落地海外服务团队的最佳时机?

    刘徽:这要从两个方面综合考量。一是要看企业想要对外塑造的形象。如果你想让客户觉得这是一家全球化的公司,那可能第一天就要有海外服务团队。它可以不是本土化的,但要保证一旦有海外客户接进来,就能及时提供海外服务。但如果公司定位是一家中国企业,那把客服、客户成功等团队布局在中国也没问题。

    二要考虑服务对象和服务类型。如果预期的服务对象是高价值的 KA 客户,服务是从上往下触达的,那更建议一开始就在对方的时区,甚至是本地,搭建海外的服务团队,以更好地提供服务。如果要服务长尾用户,或者从下往上地服务,再或者率先布局东南亚市场而不是欧美市场,那可以先把服务团队放在国内或者印度。

    LigaAI:海外服务团队是蛮丰富和立体的,它可能包括服务与技术支持(Support)、客户成功(CSM)、实施交付以及销售 BD 等等。

    对不同规模的企业而言,这些角色都需要做本地化或者全球化的部署吗?有没有可以直接「抄作业」的服务体系搭建的路径和模式能分享给大家?

    刘徽:从海外成熟企业的搭建经验来看,销售和客户成功等需要和客户直接接触、见面的团队比较适合放在本地;实施交付不需要长期维护客户关系,所以不一定要在本地。

    常见的 7 * 24 小时的 Support 团队有两种模式。第一种是整个团队都布局在印度、菲律宾这种英文好、成本低的国家,通过早、中、晚三班倒实现 7 * 24 小时的服务支持。

    第二种是像微软一样,在全球多个地区设立多个服务团队,利用时区差完成 7 * 24 小时的接力。打个比方,中国、欧洲、美国各有一个服务团队,他们各自在自己的时区内工作就可以实现 7 * 24 小时在线。但这种方式成本比较高,中小企业更建议考虑第一种方式,成本更可控。

    一般来说,业务团队(如销售和 CSM)放在本地,部署在客户所在的地方;服务与技术支持团队(Support Team)集中在一个地方——这样既方便培训,也更容易控制行为;需要进行大规模招聘或人员变更频繁的团队会放在成本比较低的地区。

    另外,我还观察到很多海外的公司(不是中国出海企业)在逐渐地将 IT 团队或者一部分产研放在中国。中国的研发资源性价比比较高,所以中国大陆是布局研发团队不错的选择。

    LigaAI:这些是更适合成熟企业,尤其中大型企业的布局方式。规模相对小一些的公司可以借鉴哪些体系搭建模式?

    刘徽:小型企业或者创业公司一般采用「本地化试点模式」除了 Support 团队要集中组建外,其他一线团队(如销售)可以先安排 1-2 人去海外尝试和探索。如果效果不错,且潜在客户体量乐观,就再扩大团队规模,而不是一上来就把销售总监、销售 VP 都招好。

    CSM 团队的布局可以稍微滞后一些。早期阶段的市场不算特别成熟,海外的本土销售更容易与客户建立信任关系,所以此时可以将销售环节前置,并让印度团队远程支持后续的服务和实施。

    当出现需面对面维护关系,且需要建立强关联度信任(即与客户方的多位成员产生联系的信任)的高价值客户时,就可以安排本土 CSM。所以,客户成功不一定要走在特别前面,可以在累积了一定数量的客户后,再尝试安排第一个本土成员。

    LigaAI:随着业务和组织规模的发展,「试点成员」未来可能会面临「当组员」还是「当组长」的问题。那 Support 团队、销售团队和 CSM 团队的「试点成员」应该以怎样的标准去招聘?

    刘徽:销售岗位更关注单兵作战的能力,只要不影响他的业绩和成单,领导是不是空降都没什么影响。所以,「试点销售」最好要招业务能力强,不太需要强管理且自觉性强的单兵战士

    对于「试点 CSM」,我建议招有潜质成为团队负责人,且能够直接上手负责客户的人。CSM 团队搭建有两种思路。一种是招「试点成员」当 Team Leader。在团队人数有限时,他先负责客户,后面再逐渐增强管理职能。另一种是先招「试点成员」满足当前的服务需求。企业就会有更宽裕的时间去预测市场规模和增长,然后再结合预测情况判断接下来要先招成员还是管理者。

    而 Support 团队建议一开始就以团队化的方式去搭建,要先确定 Team Leader。因为服务与支持团队是强体系化的,它需要明确的工作规范、绩效考核、培训制度等等,所以最好在最开始就找到有经验的管理者好好规划。

    04 高效的跨时区管理与协作

    LigaAI:SaaS 企业出海会遇到哪些跨时区协作的挑战?可以如何解决?

    刘徽:首先是跨区域成员的归属感。尽管是大家是一个团队,但如果国内同事和海外同事从来不见面,那沟通协作和磨合都会很生疏,所以大家要互相走动、串门,建立线下的连接,可以把海外成员接到中国总部来,或者总部的同事去海外看看,增强大家的归属感。

    第二是时区问题。它很难克服,但是可以通过提前规划人员配置和跨职能协作模式缓解。在为海外产品组建新团队时,首先考虑是否需要产品相关的本土成员。对于出海产品,我会比较推荐寻找海外的产品经理。

    第三,跨地区的交流和信息同步。全球化的团队应该加强跨地区的沟通、协作和分享,以便将可复制的工作经验和方法论在内部流通起来。

    最后,异步协作的信息沉淀。跨地区、跨时区的异步团队想要实现高效协作,就一定要将文档、会议的视频和音频沉淀下来,建立专属的内部知识库,并不断的完善其内容。

    LigaAI:对于不可避免的跨时区会议,如何平衡与会人的时间和信息触达效果?

    因为外国同事似乎会比较介意在非工作时段开会,但我们又希望有尽可能多的人能参加内部信息分享,这样才可以最大程度地减少信息差,避免信息辗转传递导致的损耗。

    刘徽:对,所以通常是我们(国内)来照顾海外同事的工作时间。一般情况下,会议会定在北京时间晚上 10 点左右;此时,北美大概是早上 9 点或 10 点,而欧洲正好是下午

    另外,我们会把会议分成三种类型。第一种是区域信息同步会。每个区域的内部对接人(有点类似 POC 的角色)定期开会同步近期需要跟进的工作和信息。

    第二种是内部的分享培训会。这不要求全员参与,有时间就参加,没时间也可以在结束后自行看文档或者回看录像。

    最后是每两周/一月一次的内部信息同步会,时长通常在一个小时左右,一般会要求全员参加。会议内容主要是内部政策或流程的宣贯、新工具或工作方式的引入和介绍等等;会前会将详尽的议程内容准备在文档或 PPT 里,供大家评论和批注。

    LigaAI:前面说到要尽可能将知识、经验和方法沉淀在文档中,有哪些技巧可以提高异步文档协作的效率和效果?

    刘徽:不管是 Google Docs、Notion 还是其他文档协作工具,都一定先思考清楚文档的内容结构和板块设计,最好能涵盖所有与工作相关的文档,比如培训资料、内部政策、客户文件等等。至少要保证所有想读内部文档的人能在一个固定的地方找到所有相关的资源。

    推进内部的文档协作可以尝试将文档和知识的贡献作为内部表彰和激励的一个重要考量。通过激励手段,鼓励大家主动写文档。

    另外,还可以将「文档贡献」明确进岗位晋升要求中,在岗位描述中写清楚相关的文档建设职责。比如,初级 CSM 可以只关注客户续约,但是高级 CSM 必须要定期将自己的成功经验或者客户案例在团队内部展开分享,不能只是闷声工作。


    >> LigaAI 往期精彩阅读 <<

    生成式 AI 如何释放开发者的生产力?

    新晋技术管理者如何推动组织变革?

    向上管理:三个技巧,教会你如何与上级、老板高效协作

    如何提高技术领导力?与你分享 5 个心得

    「程序员转型技术管理」必修的 10 个能力提升方向

    了解更多技术管理进阶、研发管理实践、程序员成长 等干货内容 ,欢迎关注 LigaAI。欢迎体验我们的产品,期待与你一路同行

    ]]>
    LigaAI:从效率、度量和价值维度,成为研发团队的智能医生 https://ligai.cn/blog/team/1243.html Fri, 21 Jul 2023 07:19:27 +0000 https://ligai.cn/blog/?p=1243 今年是 LigaAI 诞生的第三年。

    在过去的一千多个日夜里,我们受到很多关注,攻克了许多难关,也幸运地获得诸多客户的青睐。在稳步打磨产品,提升服务力的同时,我们始终铭记要以「客户成功」为己任,成就高效的研发协作。

    LigaAI 眼里的「客户成功」是什么样子?作为一个面向开发者的项目协作平台,LigaAI 又将以怎样的方式打破定式,赋能研发管理,真正地让客户成功?

    在这个特殊的日子,我们想将三年前埋下的那颗种子的故事分享给你。

    01 拿不到数据,证明不了团队的价值

    无论是在一线指挥有术的技术管理者,还是坐镇军帐的研发总监、CTO,在管理团队时都会被两个共性问题困扰。

    第一,无法获取真实准确的团队工作数据。 尽管已经引入研发管理工具和丰富的自动化工具,但状态更新、任务流转、通知提醒等依旧高度依赖人工完成。一方面,依靠手工提报的信息存在滞后性。 成员们往往在早会开始前一刻才将信息更新完毕,有时还会遗漏关键更新,这给管理者了解研发进度带来巨大阻碍。

    另一方面,机械重复的事务性工作和繁乱的消息通知不断打乱开发者们的工作节奏。 开发者无法专注完成真正有价值的工作,也是高质量上线交付的重要威胁。

    第二,难以衡量研发团队的业务价值。 许多企业在技术部门投入大量的财力和资源,却不能清晰地感知研发交付是否真的为企业、为业务带来了贡献。哪怕需求排期会上的每个人都坚持自己的需求是最高优先级,却没有人能说清楚「我们共同的目标是什么?」「为什么这个功能对业务很重要?」

    研发团队缺乏统一的价值感知,导致一线技术成员在构建陷阱中疲惫不堪,而研发管理者也在价值自证时举步维艰。当精细化管理取代粗放式增长,无法说清「研发如何支持业务发展」也必然成为研发团队的困身之牢。

    02 打破定式,用 AI 赋能研发管理

    「让机器帮人完成一部分工作」 是 LigaAI 产品设计的第一理念。

    我们相信,人的精力和时间应该放在更有价值的探索和创造上,而在 AI 技术发展推动着生产力和生产关系跨越式变革的今天,开发者、管理者和决策者都需要更智能的工具和平台作为大脑的延伸,进阶赋能。

    如何用 AI 技术赋能研发管理?

    LigaAI 的回答是 「以 AI 为核心,搭建价值驱动的可度量研发体系」 。具体是怎么做的?

    第一步:用智能化高效协同,解决底层数据源问题

    首先,LigaAI 从研发团队的具体工作场景入手,打通从用户反馈到开发上线的研发协作全流程;用一站式研发管理降低平台切换和数据跨平台同步的成本,为流畅的数据获取打下牢固地基。

    同时,智能助理、AI 机器人等自动化、智能化功能取代了手动更新和人工填报,状态更新、任务流转和消息通知等可以在即刻自动完成,极大地保证了研发数据的准确性和真实性。

    除了实时、干净的数据源,LigaAI 还以简洁易用的产品和功能,为开发者与管理者提供更加轻便的工作体验以及更多不被打扰的专注空间,强力支持研发提效。

    第二步:建立度量体系,提供有数可依的决策支撑

    LigaAI 融合了大量行业最佳实践和成功经验,可以结合愿景和目标为组织提供智能化、个性化的科学管理指标建议,推动行之有效的研发度量体系建设与落地。

    依靠强大的人工智能技术,LigaAI 在海量的原生过程数据中挖掘数字间的关联和意义,创新提出行业/地区性能水平等级,用「可视化趋势」代替「管理金标准」深度赋能研发管理。

    例如,Cycle Time、吞吐量等效能指标可以清晰暴露研发团队的效能瓶颈;风险预警和优化建议等辅助功能有助于识别关键问题,加快状态突破;行业标杆对比图则能引导管理者明晰组织当前的效能水平、变化趋势和成长空间,刺激有力决策。

    LigaAI 凭借科学、可靠的量化管理依据和智能决策辅助,让研发管理「有数可依」,让企业效率管理和协作优化「有据可循」。

    第三步:以价值为导向,加速产研业务一体化建设

    在保障高效协同和有数可依的基础上,LigaAI 希望让研发团队的每一次交付都为企业和业务服务

    首先,强大的集成功能让产研离用户和业务更近。 LigaAI 与 IM 工具深度融合,缩短用户反馈进入产研开发的路径,减少信息传递过程的损耗与失真。多级帐号体系进一步简化用户反馈到用户故事的处理步长,提高成果交付和对外反馈的沟通效率。

    其次,量化业务价值,让研发贡献更加清晰。 LigaAI 设计了一套「价值因子评分体系」,将其与最佳实践和方法论结合可以引导研发团队逐步拆解、绘制与目标相关的价值因子矩阵,将故事价值具象化、标准化。在对外协作中,价值量化也会驱动研发团队拉齐跨部门、跨组织的目标共识和价值认知,让所有人理解「为什么要做这个功能」。

    最后,定期复盘价值假设,修正并调整偏离对象。 用科学度量取代「拍脑袋决策」后,LigaAI 将目标偏离度、价值贡献度等成果数据可视化,辅以智能分析、指标优化等决策建议,为研发管理者和 C-Level 呈现一个专业、清晰的全局管理视图,惠及更长远的规划和管理。

    03 重新定义「优秀的研发团队」

    LigaAI 认为,优秀的研发团队既要有扎实强悍的研发内功,也要具备价值导向的文化内核。

    研发内功是研发团队快速交付高质量产品的能力。优秀的研发团队会内驱地不断探索更加简单、更加高效的工作方式,持续进步、持续成长。

    文化内核是一个团队开展工作的底线,会潜移默化地引导企业和组织往正确的方向前进。我们相信只有足够贴近市场、贴近业务,始终围绕(业务)目标攻克正确挑战的企业和团队,才能不被市场抛弃或淘汰。

    坚持用正确的方式做正确的事情,永远保持探索欲和好奇心,然后才能在变化和变动中有韧性地生存下来,突破,再增长。

    04 成为研发团队的智能医生

    从开发者和研发管理者的具体工作场景出发,我们将在研发领域沉淀了十余年的实战经验、对高效生产力工具的狂热追求,以及对业务价值的洞察和理解融入产品,为广大研发团队打造出一款能适应更广泛协作的、以 AI 技术为核心的数据化、智能化的研发协作平台——LigaAI

    我们希望能以 AI 技术赋能,打造价值驱动的可度量研发体系,让混乱、复杂的研发工作和研发管理变轻松、变简单,让开发者、管理者和决策者均从中受益。

    人工智能时代,LigaAI 想要成为千千万万个研发团队的智能医生,陪伴每一个研发团队升级打怪,探索未来,进阶成长。

    关于 LigaAI

    LigaAI 是一家以人工智能技术为核心的企业服务公司,致力于通过 AI 场景,化繁为简,提升协作效率,赋能广大研发团队。从开发者的具体工作场景出发,LigaAI 提供了强大的工具集成能力,为开发者提供简洁、智能的协作体验,为不同类型的组织提供数字化、个性化、智能化的项目协作平台。

    脱繁复,拥抱简单,让研发协作回归价值交付。

    欢迎注册使用 LigaAI 新一代智能研发协作平台。

    ]]>
    3 个技巧,让你像技术专家一样解决编码问题 https://ligai.cn/blog/%e7%a0%94%e5%8f%91%e7%ae%a1%e7%90%86/1232.html Mon, 12 Jun 2023 04:08:37 +0000 https://ligai.cn/blog/?p=1232 阅读更多]]> 「我应该如何提高解决问题的能力?尽管我掌握了 JavaScript,却无法解决实际问题或理解复杂的 JavaScript 代码。」

    经常有年轻的开发者朋友问我类似的问题。对开发者来说,解决问题非常重要。编写优秀的代码是一门创造性的艺术,而要成为出色的开发者,就必须成为富有创造力的能够解决问题的人。

    我发现新手程序员犯的最大的错误是专注于学习语法,而不是学习如何解决问题。—— V. Anton Spraul

    尽管我观察到,解决问题的技能需要时间和经验的积累,但我坚信掌握它并不需要很多年;只要勇敢地直面问题,就会有所提高。我曾与许多初级开发人员一起工作,年轻人们也比他们的老伙计更善于解决问题。

    本文将详细讲解三个技巧,让开发者可以像技术专家一样解决问题。重头戏开始之前,我们先来看看技术专家和技术小白在解决问题方面有哪些不同。

    01 专家思维 vs 新手思维

    大多数人会回避或胡乱处理问题。优秀的思考者和领导者则会主动寻找问题,他们也拥有能够更好地解决问题的方法。—— Michael Simmons

    Robert J. Sternberg 教授根据美国心理学家 Herbert A. Simon、Robert Glaser 和 Micheline Chi 等人的研究,揭示了技术专家解决问题比技术小白更有效的秘密。

    Arnaud Chevallier 在 Work forward in solving problems, not backward 一文中犀利写道,「逆向工作法是一种从假设出发的方法。如果想要提高利润率,逆向工作法会指引你去寻找增加收入的办法,因为增加收入可以带来更高的利润率。那减少成本呢?难道在确定最终的解决方案之前,我们不应该先全面地了解各种可以实现目标的办法吗?」

    可以看到,技术专家通常会花大量的时间寻找、明确和定义问题,并且使用正向工作法解决问题,同时密切关注问题解决的过程。下面就跟大家分享,技术专家们常用的问题解决技巧。

    02 三个专家级的问题解决技巧

    1. 问题十二连 The 12 What Elses

    提问题听上去没什么难度,但要找到正确的、缺失的问题并准确地描述出来却不容易。Lenedra J. Carroll 介绍的「问题十二连 The 12 What Elses」可以有效帮助我们摆脱这个苦恼。

    在头脑风暴时,先提出一个问题,并生成 12 个答案;然后选取其中一个答案转化为下一个问题,再生成 12 个答案。不断重复此过程,直到获得一个明确的解决方案。

    通过连续地提问,我们会得到一个「问题回答地图」,它对假设的测试和结构化解决复杂问题很有帮助。

    使用「问题回答地图」测试假设

    提问和追问是如何将我们往正确方向上引导的?下面两个例子可能会给你答案。

    2. 根本原因分析法 Root Cause Analysis

    我们经常在多次解决失败后,才发现问题的情况跟预期有所不同,所以在开始解决问题之前,就要先了解其根本原因是什么。

    只有消除错误的选择,才能更好地定义问题并找到有效的解决方案。根本原因分析法有助于避免在错误的方向上浪费时间和精力。

    根本原因分析的几种方法
    根本原因分析法的示例

    当需要修复 Bug 时,开发者可以使用以下任意方式,进行根本原因分析:

    • 确定问题在哪个环境出现,并尝试在相同和不同的环境中重现它,以掌握更好的理解。
    • 如果与 Web 性能有关,可以分析捆绑文件。
    • 进行单元测试和集成测试。
    • 进行日志文件分析。
    • 进行交互式调试。

    3. 使用多元思维 Spectrum Thinking

    二元思维认为事情的状态是非黑即白的,只有互相对立的两种可能。有些时候它是正确的,但其他时候,它可能是一种错误的简化。

    二元思维

    与二元思维对应的是多元思维,也可以称作频谱思维(Spectrum Thinking)。它会考虑更多选择、更多替代方案和可能性,比如「两者共存」「介于两者之间」「其他的可能性」或「二者皆否」等。

    多元思维

    通过培养多元思维,开发者可以有效提升创造力;你会惊讶地发现,修复 Bug、解决冲突、设计/执行客户需求的实现方案等居然会有这么多种解决方案和方式。

    以展示信息详情为例,二元思维认为,信息详情要么通过弹窗展示,要么跳转到一个带返回箭头的新页面进行展示。

    多元思维认为还有其他可能性,比如新增 Tab 页直接查看和更新信息,无需关闭当前列表页面。

    多元思维还可能认为,可以提供一个支持三种布局的动态模板,让用户自主选择要用以上哪种方式。

    二元思维和多元思维各有利弊,在实际工作中可以配合使用。

    写在最后

    解决问题能力是一个超出软件开发范畴的话题,它高度取决于我们的心态和态度。要想培养和提高解决(复杂)问题的能力,首先要对问题和挑战充满好奇心,而不是感到沮丧。

    就像 Tim Hicks 说的那样,「问题就像赛车道上的弯道。处理得好,便可以在接下来的直道中状态满分;如果过弯太快,很可能会引发侧翻,影响后续赛程。」

    (原文作者:Rakia Ben Sassi)


    >> LigaAI 往期精彩阅读 <<

    ChatGPT 之后,B 端产品设计会迎来颠覆式革命吗?

    技术分享 | 如何编写同时兼容 Vue2 和 Vue3 的代码?

    产品经理的 5 种错误打开方式,你中招了吗?

    什么是研发 Lead Time?我终于掰扯明白了!

    了解更多开发者提效、研发效能管理、前沿技术等消息,欢迎关注 LigaAI。LigaAI 助力开发者扬帆远航,欢迎体验我们的产品,期待与你一路同行!

    ]]>