langsmith hub로 살펴보는 prompt

미래의 코딩은 인간이 직접 작성하는 code 비중 보다 prompt의 비중이 더 커질 것은 당연한 사실이다. 또한 실제 코드 작성은 인간보다 AI가 작성하는 양이 더 많아 질 것이다. 현재 시점에서 나는 prompt가 곧 code라고 생각한다. 물론 가까운 시점에 이런 생각은 맞지 않겠지만 말이다. 예를들어 code로 결과를 OK나 DENY라는 문자열 중 하나만을 얻어야 할때 어떻게 코드를 작성할 것인가.. 라는 물음에 대한 코드를 대신하는 프롬프트는 ‘너는 결과를 반드시 “OK” 또는 “DENY” 둘 중에 하나로만 답해야 한다.’ 정도가 된다. 결국 LLM을 통해 좀더 정확한 결과를 얻기 위해서 프롬프트의 작성 방법을 이해하는 것은 개발자의 핵심 역량이다.

이처럼 중요한 프롬프트에 대한 좋은 예시를 통해 프롬프트에 대한 작성 방법을 학습할 수 있다. 좋은 프롬프트 예시는 어디서 구할 수 있을까? langchain 프레임워크의 소스코드를 살펴보면 코드 곳곳에 사용된 프롬프트가 존재하며 이들 역시 좋은 예시다. 추가로 langchain의 개발사는 langsmith hub를 통해 prompt를 등록할 수 있도록 해 두었는데, 여기에 등록된 프롬프트 중 사용 횟수가 많은 놈들을 분석해 보는 것을 강력히 추천한다. 그 중 3개 정도만 분석해 보자.

먼저 2천7백만번 사용된 rlm/rag-prompt이다. RAG를 통해 context를 제공하는 형태이다.

human

You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.
Question: {question} 
Context: {context} 
Answer:

다음은 8백만번 사용된 프롬프트인 hwchase17/react 이다.

human

Answer the following questions as best you can. You have access to the following tools:

{tools}

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: {input}
Thought:{agent_scratchpad}

마지막으로 5만번 정도 사용된 hardkothari/prompt-marker 의 프롬프트는 다음과 같다. 프롬프트를 좀더 개선하기 위한 용도이다.

system

You are an expert Prompt Writer for Large Language Models.

human

Your goal is to improve the prompt given below for {task} :
--------------------

Prompt: {lazy_prompt}
--------------------

Here are several tips on writing great prompts:
-------

Start the prompt by stating that it is an expert in the subject.
Put instructions at the beginning of the prompt and use ### or to separate the instruction and context 
Be specific, descriptive and as detailed as possible about the desired context, outcome, length, format, style, etc 
---------

Here's an example of a great prompt:
As a master YouTube content creator, develop an engaging script that revolves around the theme of "Exploring Ancient Ruins."
Your script should encompass exciting discoveries, historical insights, and a sense of adventure.
Include a mix of on-screen narration, engaging visuals, and possibly interactions with co-hosts or experts.
The script should ideally result in a video of around 10-15 minutes, providing viewers with a captivating journey through the secrets of the past.

Example:
"Welcome back, fellow history enthusiasts, to our channel! Today, we embark on a thrilling expedition..."
-----

Now, improve the prompt.

IMPROVED PROMPT:

언급된 프롬프트를 살펴보니, 이런 다소 어려운 프롬프트들을 잘도 이해하는 LLM이 대단스럽단 생각이 든다. 개인적으로 나는 프롬프트를 위처럼 작성하지 않고 마크다운 형식으로 작성한다.

uv로 파이썬 프로젝트 생성

먼저 터미널로 프로젝트를 구성할 폴더로 이동하고 ..

uv init

위의 명령이 실행되면 해당 폴더에 프로젝트 구성 파일이 생성된다. 그중 main.py 파일이 보이는데 이 파일을 다음처럼 실행하면 필요한 가상환경이 생성되면서 해당 py 파일이 실행된다.

uv run main.py

참고로 uv는 다음처럼 설치할 수 있다.

pip install uv

uv 통한 패키지 설치는 다음과 같다. (예: requests)

uv add requests

위의 방식을 권장하지만 다음으로도 가능하다.

uv pip install requests

GLSL vs TSL Nodes

GLSL TSL
position positionGeometry
transformed positionLocal
transformedNormal normalLocal
vWorldPosition positionWorld
normalWorld
gl_FrongFacing frontFacing
vColor vertexColor()
vUv / uv uv()
vNormal normalView
cameraPosition
cameraPosition
cameraNear
cameraFar
modelMatrix modelWorldMatrix
modelViewMatrix modelViewMatrix
viewMatrix cameraViewMatrix
projectionMatrix cameraProjectionMatrix
diffuseColor material.colorNode
gl_FragColor material.fragmentNode
texture2D(tex, uv) texture(tex, uv)
textureCube(tex, dir) cubeTexture(tex, dir)
gl_FragCoord screenCoordinate
screenSize
gl_PointCoord SpriteNodeMaterial/PointsNodeMaterial에서 uv()
gl_InstanceID instanceIndex
gl_VertexIndex vertexIndex
time
deltaTime
frameId

THREE.JS 퀵 레퍼런스 코드

Three.js를 이용한 개발 시 개인적으로 빠르게 참조하기 위해 작성한 글입니다.

three.js 기본 프로젝트 생성 (WebGL)

git clone https://github.com/GISDEVCODE/threejs-with-javascript-starter.git .

three.js 기본 프로젝트 생성 (WebGPU)

git clone https://github.com/GISDEVCODE/threejs-webgpu-with-javascript-starter.git .

three.js 기본 프로젝트 생성 (WebGPU + Typescript)

git clone https://github.com/GISDEVCODE/threejs-webgpu-with-typescript-starter.git .

R3F 기본 프로젝트 생성 (Javascript)

git clone https://github.com/GISDEVCODE/r3f-with-javascript-starter.git .

Shader 기본 프로젝트 생성 (Javascript)

git clone https://github.com/GISDEVCODE/shader-with-threejs-javascript-starter .

패키지 최신 버전으로 업데이트

npx npm-check-updates -u

그림자 적용에 대한 코드

renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.VSMShadowMap;

const shadowLight = new THREE.DirectionalLight(0xffe79d, 0.7);
shadowLight.position.set(150, 220, 100);
shadowLight.target.position.set(0,0,0);
shadowLight.castShadow = true;
shadowLight.shadow.mapSize.width = 1024*10;
shadowLight.shadow.mapSize.height = 1024*10;
shadowLight.shadow.camera.top = shadowLight.shadow.camera.right = 1000;
shadowLight.shadow.camera.bottom = shadowLight.shadow.camera.left = -1000;
shadowLight.shadow.camera.far = 800;
shadowLight.shadow.radius = 5;
shadowLight.shadow.blurSamples = 5;
shadowLight.shadow.bias = -0.0002;

const cameraHelper = new THREE.CameraHelper(shadowLight.shadow.camera);
this._scene.add(cameraHelper);

island.receiveShadow = true;
island.castShadow = true;

지오메트리의 좌표 수정

const sphereGeom = new THREE.SphereGeometry(6 + Math.floor(Math.random() * 12), 8, 8);
const sphereGeomPosition = sphereGeom.attributes.position;
for (var i = 0; i < sphereGeomPosition.count; i++) {
    sphereGeomPosition.setY(i, sphereGeomPosition.getY(i) + Math.random() * 4 - 2);
    sphereGeomPosition.setX(i, sphereGeomPosition.getX(i) + Math.random() * 3 - 1.5);
    sphereGeomPosition.setZ(i, sphereGeomPosition.getZ(i) + Math.random() * 3 - 1.5);
}

sphereGeom.computeVertexNormals();
sphereGeom.attributes.position.needsUpdate = true;

지오메트리에 사용자 정의 데이터 주입

// 주입
const waves = [];
const waterGeoPositions = waterGeo.attributes.position;
for (let i = 0; i < waterGeoPositions.count; i++) {
    waves[i] = Math.random() * 100;
}
waterGeo.setAttribute("wave", new THREE.Float32BufferAttribute(waves, 1));

// 읽기
const waves = sea.geometry.attributes.wave;

for(let i=0; i<positions.count; i++) {
    const v = waves.getX(i);
}

안개 설정 코드

scene.fog = new THREE.Fog("rgba(54,219,214,1)", 1000, 1400);

OrbitControls 관련 코드

const controls = new OrbitControls(this._camera, this._divContainer);
controls.minPolarAngle = -Math.PI / 2;
controls.maxPolarAngle = Math.PI / 2 + 0.1;
controls.enableZoom = true;
controls.enablePan = false;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.2;

this._controls = controls;

this._controls.update();

Object3D의 MBR 얻기

const board = this._scene.getObjectByName("Board");
const box = new THREE.Box3().setFromObject(board);
console.log(box);

Mesh의 월드좌표에 대한 position 얻기

mesh.updateMatrixWorld();

const worldPos = new THREE.Vector3();
worldPos.setFromMatrixPosition(mesh.matrixWorld);

Faked Shadow

그림자를 위한 매시에 대한 재질 속성 지정이 핵심. 참고로 shadow에 대한 이미지는 투명 이미지가 아님. 즉, 배경색이 하얀색인 이미지임.

const shadow = new THREE.TextureLoader().load( 'models/gltf/ferrari_ao.png' );

const mesh = new THREE.Mesh(
    new THREE.PlaneGeometry( 0.655 * 4, 1.3 * 4 ),
    new THREE.MeshBasicMaterial( {
        map: shadow, 
        blending: THREE.MultiplyBlending, 
        toneMapped: false, 
        transparent: true
    } )
);
mesh.rotation.x = - Math.PI / 2;
mesh.renderOrder = 2;
carModel.add( mesh );

텍스쳐 이미지 품질 올리기

샘플링 횟수를 올리는 것으로 속도는 느려질 수 있으나 품질은 향상됨

texture.anisotropy = renderer.capabilities.getMaxAnisotropy();

async 리소스 로딩

async function init() {
    const rgbeLoader = new RGBELoader().setPath('textures/equirectangular/');
    const gltfLoader = new GLTFLoader().setPath('models/gltf/DamagedHelmet/glTF/');

    const [texture, gltf] = await Promise.all([
        rgbeLoader.loadAsync( 'venice_sunset_1k.hdr' ),
        gltfLoader.loadAsync( 'DamagedHelmet.gltf' ),
    ]);
}

init().catch(function(err) {
    console.error(err);
});

텍스쳐를 Canvas로 후다닥 만들기

const canvas = document.createElement( 'canvas' );
canvas.width = 1;
canvas.height = 32;

const context = canvas.getContext( '2d' );
const gradient = context.createLinearGradient( 0, 0, 0, 32 );
gradient.addColorStop( 0.0, '#ff0000' );
gradient.addColorStop( 0.5, '#00ff00' );
gradient.addColorStop( 1.0, '#0000ff' );
context.fillStyle = gradient;
context.fillRect( 0, 0, 1, 32 );

const sky = new THREE.Mesh(
	new THREE.SphereGeometry( 10 ),
	new THREE.MeshBasicMaterial( { map: new THREE.CanvasTexture( canvas ), side: THREE.BackSide } )
);
scene.add( sky );

GLTF 파일 로딩

import { GLTFLoader } from "../examples/jsm/loaders/GLTFLoader.js"

const loader = new GLTFLoader();
loader.load("./data/ring.glb", gltf => {
    const object = gltf.scene;
    this._scene.add(object);
});    

InstancedMesh

const mesh = new THREE.InstancedMesh(geometry, material, 10000)

const matrix = new THREE.Matrix4()
const dummy = new THREE.Object3D()
 
for(let i = 0; i < 10000; i++) {
  mesh.getMatrixAt(i, matrix)
  matrix.decompose(dummy.position, dummy.rotation, dummy.scale)
  
  dummy.rotation.x = Math.random()
  dummy.rotation.y = Math.random()
  dummy.rotation.z = Math.random()

  dummy.updateMatrix()

  mesh.setMatrixAt(i, dummy.matrix)
  mesh.setColorAt(i, new THREE.Color(Math.random() * 0xffffff)
}

mesh.instanceMatrix.needsUpdate()

Image 기반 광원(IBL)

import { RGBELoader } from 'three/examples/jsm/Addons.js'

...

new RGBELoader().setPath("./").load("pine_attic_2k.hdr", (data) => {
  data.mapping = THREE.EquirectangularReflectionMapping;
  // this.scene.background = data;
  // this.scene.backgroundBlurriness = 0.6;
  this.scene.environment = data;
})

GLTF / GLB 파일 로딩

import { GLTFLoader, OrbitControls } from "three/addons/Addons.js"

...

const loader = new GLTFLoader();
loader.load(
  "fileName.glb",
  (gltf) => {
    this._scene.add( gltf.scene );    
  },
  (xhr) => { console.log( ( xhr.loaded / xhr.total * 100 ) + "% loaded" ); },
  (error) => { console.log( "An error happened" ); }
);

Object3D를 빌보드로 만들기

this._mesh.quaternion.copy(this._camera.quaternion );
// or
this._mesh.rotation.setFromRotationMatrix( this._camera.matrix );

FPS 측정

let frameCount = 0;
let lastTime = performance.now();
let fps = 60;

function updateFPS() { // 렌더링 함수에서 호출해야 함
  frameCount++;
  const currentTime = performance.now();
  const deltaTime = currentTime - lastTime;

  if (deltaTime >= 1000) {
    fps = Math.round((frameCount * 1000) / deltaTime);
    frameCount = 0;
    lastTime = currentTime;
  }
}

FPS 제한하기

requestAnimationFrame로 렌더링을 수행하면 최대한 많은 프레임을 생성하기 됨. 아래는 원하는 프레임수로 제한하기 위해 다음 코드로 30 프레임 제한입니다.

  _elapsedTime = 0;
  _fps = 1 / 60
  render() {
    const delta = this._clock.getDelta();
    this.update(delta);
    this._elapsedTime += delta;
    
    if (this._elapsedTime >= (this._fps)) {
      this._stats.begin();
      this._renderer.render(this._scene, this._camera);
      this._stats.end();
      this._elapsedTime %= this._fps;
    }

    requestAnimationFrame(this.render.bind(this));
  }

화면 좌표를 월드 좌표로 변환하는 함수

// 스크린 좌표 -> 월드 좌표 변환 함수
screenToWorld(screenX, screenY, ndcZ = 0.5) { // ndcZ=0.5로 설정 (카메라 방향)
  // 1. 화면 픽셀 좌표를 정규화된 디바이스 좌표(NDC)로 변환
  const ndcX = (screenX / this._divContainer.clientWidth) * 2 - 1;
  const ndcY = -(screenY / this._divContainer.clientHeight) * 2 + 1;

  // 2. NDC를 3D 공간의 Ray로 변환
  const vector = new THREE.Vector3(ndcX, ndcY, ndcZ);
  vector.unproject(this._camera);

  // 3. Ray를 따라 월드 좌표를 계산
  return vector;
}

screenToWorldAtZ(screenX, screenY, targetZ = 0) {
  // 1. 화면 픽셀 좌표 -> NDC 변환
  const ndcX = (screenX / this._divContainer.clientWidth) * 2 - 1;
  const ndcY = -(screenY / this._divContainer.clientHeight) * 2 + 1;

  // 2. NDC -> Ray 생성
  const raycaster = new THREE.Raycaster();
  const vector = new THREE.Vector2(ndcX, ndcY);
  raycaster.setFromCamera(vector, this._camera);

  // 3. Ray와 z=targetZ 평면의 교차점 계산
  const direction = raycaster.ray.direction;
  const origin = raycaster.ray.origin;
  const t = (targetZ - origin.z) / direction.z;

  const worldPoint = origin.clone().add(direction.multiplyScalar(t));
  return worldPoint;
}

모델을 화면 중심에 표시하기

_zoomFix(model, distanceFactor = 1.5) {
  const box = new THREE.Box3().setFromObject(model);
  const center = box.getCenter(new THREE.Vector3());

  model.position.sub(center);

  const size = box.getSize(new THREE.Vector3());
  const maxDimension = Math.max(size.x, size.y, size.z);
  const cameraDistance = maxDimension * distanceFactor;
  this._camera.position.set(0, 0, cameraDistance);
  this._camera.lookAt(this._scene.position);
}

모델의 특정 페이스에 대한 법선 벡터 구하기

_getNormal(mesh, faceIndex) {
  const normalAttribute = mesh.geometry.getAttribute("normal");
  const index = mesh.geometry.index;
  const faceStartIndex = faceIndex * 3;

  const vertexIndexA = index.getX(faceStartIndex);
  const vertexIndexB = index.getX(faceStartIndex + 1);
  const vertexIndexC = index.getX(faceStartIndex + 2);

  const normalA = new THREE.Vector3();
  const normalB = new THREE.Vector3();
  const normalC = new THREE.Vector3();
  const faceNormal = new THREE.Vector3();

  normalA.fromBufferAttribute(normalAttribute, vertexIndexA);
  normalB.fromBufferAttribute(normalAttribute, vertexIndexB);
  normalC.fromBufferAttribute(normalAttribute, vertexIndexC);

  faceNormal.addVectors(normalA, normalB).add(normalC).divideScalar(3).normalize();

  return faceNormal;
}

어떤 위치에서 매시에 가장 가까운 매시 표면의 위치를 얻는 코드

아래 함수의 point 파라메터는 어떤 위치이고 mesh 파라메터가 대상 메시입니다. point에서 가장 가까운 매시 표면의 좌표가 변환됩니다.

findClosestPointOnMesh(/* Vector3 */ point, /* Mesh */ mesh) {
  const geometry = mesh.geometry;
  const vertices = geometry.attributes.position;
  const indices = geometry.index ? geometry.index.array : null;

  let closestPoint = new THREE.Vector3();
  let minDistance = Infinity;

  const localPoint = mesh.worldToLocal(point);

  if (indices) {
    for (let i = 0; i < indices.length; i += 3) {
      const a = new THREE.Vector3().fromBufferAttribute(vertices, indices[i]);
      const b = new THREE.Vector3().fromBufferAttribute(vertices, indices[i + 1]);
      const c = new THREE.Vector3().fromBufferAttribute(vertices, indices[i + 2]);

      const tempPoint = new THREE.Vector3();
      const triangle = new THREE.Triangle(a, b, c);
      triangle.closestPointToPoint(localPoint, tempPoint);

      const distance = localPoint.distanceTo(tempPoint);
      if (distance < minDistance) {
        minDistance = distance;
        closestPoint.copy(tempPoint);
      }
    }
  } else {
    for (let i = 0; i < vertices.count; i += 3) {
      const a = new THREE.Vector3().fromBufferAttribute(vertices, i);
      const b = new THREE.Vector3().fromBufferAttribute(vertices, i + 1);
      const c = new THREE.Vector3().fromBufferAttribute(vertices, i + 2);

      const tempPoint = new THREE.Vector3();
      const triangle = new THREE.Triangle(a, b, c);
      triangle.closestPointToPoint(localPoint, tempPoint);

      const distance = localPoint.distanceTo(tempPoint);
      if (distance < minDistance) {
        minDistance = distance;
        closestPoint.copy(tempPoint);
      }
    }
  }

  return mesh.localToWorld(closestPoint);
}

아래는 위의 함수가 적용된 실행 화면인데, 노란색 포인트 위치가 임의의 위치(point 파라메터)이고 빨간색이 함수의 결과 좌표입니다.

DataTexture를 이용한 Raw 데이터로 텍스쳐 생성

    const width = 256;
    const height = 256;
    const size = width * height;
    const data = new Uint8Array(4 * size); // RGBA 데이터

    for (let i = 0; i < size; i++) {
      const stride = i * 4;

      data[stride] = Math.floor(Math.random() * 256);     // R
      data[stride + 1] = Math.floor(Math.random() * 256); // G
      data[stride + 2] = Math.floor(Math.random() * 256); // B
      data[stride + 3] = 255;                             // A
    }

    const texture = new THREE.DataTexture(data, width, height);
    texture.needsUpdate = true;

    const material = new THREE.MeshBasicMaterial({ map: texture });
    const mesh = new THREE.Mesh(new THREE.PlaneGeometry(4, 4), material);
    this._scene.add(mesh);

여러 개의 지오메트리들을 한 개의 지오메트리로 병합하기

const mat3 = new THREE.MeshBasicMaterial();
const pumpkin = await this.loadGLTF('./resources/models/pumpkin.glb');
const geos = [];

// 아래의 2줄에 대한 변환 코드는 updateMatrixWorld 매서드가 실행되면 변환 값이 matrixWorld에 반영됨
pumpkin.children[0].position.set(0, 0, 0);
pumpkin.children[0].scale.setScalar(0.01);

pumpkin.children[0].traverse(child => {
  child.updateMatrixWorld(); 
  if(child.isMesh) {
    if(child.geometry) {
      const attribute = child.geometry.getAttribute('position').clone();
      const geometry = new THREE.BufferGeometry();
      geometry.setIndex(child.geometry.index);
      geometry.setAttribute('position', attribute);
      geometry.applyMatrix4(child.matrixWorld);
      geos.push(geometry);
    }
  }
});
const combinedGeometry = BufferGeometryUtils.mergeGeometries(geos);
const combinedMesh = new THREE.Mesh(combinedGeometry, mat3);

마우스 커서 위치에 대한 3차원 공간 좌표(평면 기준) 얻기

const mouse3D = new THREE.Vector3(0, 0, 0);
const raycaster = new THREE.Raycaster();
const intersectionPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);

window.addEventListener('mousemove', (event) => {
  const mouse = new THREE.Vector2(
    (event.clientX / window.innerWidth) * 2 - 1,
    -(event.clientY / window.innerHeight) * 2 + 1
  );
  raycaster.setFromCamera(mouse, camera);

  // Plane 수학 모델을 통한 마우스 커서의 Plane 상의 3차원 위치 얻기(mouse3D에 저장)
  raycaster.ray.intersectPlane(intersectionPlane, mouse3D);
});

OrbitControls 등으로 카메라가 회전될때 Plane 역시 카메라 방향으로 회전시켜야 하는데, 이에 대한 코드는 다음과 같다.

...
const intersectionPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const helper = new THREE.PlaneHelper(plane, 1, 0xffff00);
this.scene.add(helper);

window.addEventListener('mousemove', (event) => {
  ...
  raycaster.setFromCamera(mouse, camera);

  // plane을 항상 카메라 방향을 향하도록 하기
  const normal = new THREE.Vector3();
  this.camera.getWorldDirection(normal);

  // plane normal을 카메라 방향으로 설정
  plane.normal.copy(normal.negate());

  // plane을 카메라 위치에 맞추고 싶다면
  // plane.constant = - this.camera.position.dot(plane.normal);

  // helper.updateMatrixWorld(true); // optional
  // .

  // Plane 수학 모델을 통한 마우스 커서의 Plane 상의 3차원 위치 얻기(mouse3D에 저장)
  raycaster.ray.intersectPlane(intersectionPlane, mouse3D);
});

TSL 노드함수인 instancedArray로 생성된 GPU 메모리를 CPU 메모리의 배열형태로 가져오기

this.velocityBuffer = instancedArray(this.COUNT, 'vec3');

...

const resultBuffer = await renderer.getArrayBufferAsync(this.velocityBuffer.value);
const resultArray = new Float32Array(resultBuffer);
console.log(resultArray)

billboarding TSL

material.vertexNode = billboarding({ horizontal: true, vertical: true });

편미분에 의한 노말 벡터 구하기

const normalVector = Fn(([pos]) => {
  const dU = dFdx(pos).toConst(), dV = dFdy(pos).toConst();
  return transformNormalToView(cross(dU, dV).normalize());
  // return cross(dU, dV).normalize();
});

normalVector의 인자값으로 positionLocal 노드가 올 수 있다.